
Security News
Axios Maintainer Confirms Social Engineering Attack Behind npm Compromise
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.
@telegraph/input
Advanced tools
Flexible input component with slots for icons, buttons, and custom elements.
npm install @telegraph/input
Pick one:
Via CSS (preferred):
@import "@telegraph/input";
Via Javascript:
import "@telegraph/input/default.css";
Then, include
className="tgph"on the farthest parent element wrapping the telegraph components
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Search } from "lucide-react";
export const SearchInput = () => (
<Input
placeholder="Search..."
LeadingComponent={<Icon icon={Search} alt="Search" />}
/>
);
<Input> (Default Component)The default Input component provides a simple API for common use cases.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "1" | "2" | "3" | "2" | Input size |
variant | "outline" | "ghost" | "outline" | Visual style variant |
errored | boolean | false | Shows error state styling |
disabled | boolean | false | Disables the input |
LeadingComponent | React.ReactNode | undefined | Component to display before the input text |
TrailingComponent | React.ReactNode | undefined | Component to display after the input text |
textProps | object | undefined | Props to pass to the inner Text component |
stackProps | object | undefined | Props to pass to the Stack container |
as | TgphElement | "input" | HTML element or component to render |
For advanced use cases, use the composition API:
<Input.Root>Container component that wraps the input and slots.
| Prop | Type | Default | Description |
|---|---|---|---|
size | "1" | "2" | "3" | "2" | Input size |
variant | "outline" | "ghost" | "outline" | Visual style variant |
errored | boolean | false | Shows error state styling |
disabled | boolean | false | Disables the input |
textProps | object | undefined | Props to pass to the inner Text component |
stackProps | object | undefined | Props to pass to the Stack container |
as | TgphElement | "input" | HTML element or component to render |
<Input.Slot>Wrapper for leading and trailing components.
| Prop | Type | Default | Description |
|---|---|---|---|
position | "leading" | "trailing" | "leading" | Position of the slot |
import { Input } from "@telegraph/input";
// Simple text input
<Input placeholder="Enter your name" />
// Email input
<Input type="email" placeholder="Enter your email" />
// Password input
<Input type="password" placeholder="Enter your password" />
import { Input } from "@telegraph/input";
// Small input
<Input size="1" placeholder="Small" />
// Medium input (default)
<Input size="2" placeholder="Medium" />
// Large input
<Input size="3" placeholder="Large" />
import { Input } from "@telegraph/input";
// Outline variant (default)
<Input variant="outline" placeholder="Outlined input" />
// Ghost variant
<Input variant="ghost" placeholder="Ghost input" />
import { Input } from "@telegraph/input";
// Error state
<Input errored placeholder="This has an error" />
// Disabled state
<Input disabled placeholder="This is disabled" />
import { Input } from "@telegraph/input";
import { Icon } from "@telegraph/icon";
import { Search, Mail, Lock } from "lucide-react";
// Search input
<Input
placeholder="Search..."
LeadingComponent={<Icon icon={Search} alt="Search" />}
/>
// Email input
<Input
type="email"
placeholder="Enter email"
LeadingComponent={<Icon icon={Mail} alt="Email" />}
/>
// Password input with toggle
<Input
type="password"
placeholder="Password"
LeadingComponent={<Icon icon={Lock} alt="Password" />}
TrailingComponent={
<Icon
icon={Eye}
alt="Toggle password visibility"
onClick={togglePasswordVisibility}
/>
}
/>
Use stackProps to customize the container and textProps to customize the input field:
import { Input } from "@telegraph/input";
// Full-width input with custom padding
<Input
placeholder="Search..."
stackProps={{
w: "full",
padding: "3",
}}
/>
// Custom text styling
<Input
placeholder="Enter code"
textProps={{
fontFamily: "mono",
letterSpacing: "wider",
}}
/>
// Combined customization
<Input
placeholder="Custom input"
stackProps={{
w: "full",
rounded: "4",
border: "2",
}}
textProps={{
weight: "medium",
}}
/>
Use Input.Root and Input.Slot for maximum control:
import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Filter, Search } from "lucide-react";
export const AdvancedSearchInput = () => (
<Input.Root placeholder="Search products...">
<Input.Slot position="leading">
<Icon icon={Search} alt="Search" />
</Input.Slot>
<Input.Slot position="trailing">
<Button
variant="ghost"
size="1"
icon={{ icon: Filter, alt: "Filter" }}
onClick={openFilters}
/>
</Input.Slot>
</Input.Root>
);
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Stack } from "@telegraph/layout";
import { Text } from "@telegraph/typography";
import { AlertCircle } from "lucide-react";
import { useState } from "react";
export const FormField = () => {
const [value, setValue] = useState("");
const [error, setError] = useState("");
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setValue(e.target.value);
if (error) setError(""); // Clear error on change
};
return (
<Stack gap="1">
<Text as="label" htmlFor="email">
Email Address
</Text>
<Input
id="email"
type="email"
value={value}
onChange={handleChange}
errored={!!error}
placeholder="Enter your email"
LeadingComponent={<Icon icon={Mail} alt="Email" />}
/>
{error && (
<Stack direction="row" align="center" gap="1">
<Icon icon={AlertCircle} alt="Error" color="red" size="1" />
<Text size="1" color="red">
{error}
</Text>
</Stack>
)}
</Stack>
);
};
import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Search, X } from "lucide-react";
import { useState } from "react";
export const SearchInput = () => {
const [query, setQuery] = useState("");
const clearSearch = () => setQuery("");
return (
<Input
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search..."
LeadingComponent={<Icon icon={Search} alt="Search" />}
TrailingComponent={
query && (
<Button
variant="ghost"
size="1"
icon={{ icon: X, alt: "Clear search" }}
onClick={clearSearch}
/>
)
}
/>
);
};
import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Minus, Plus } from "lucide-react";
import { useState } from "react";
export const NumberInput = ({ min = 0, max = 100 }) => {
const [value, setValue] = useState(0);
const increment = () => setValue((prev) => Math.min(prev + 1, max));
const decrement = () => setValue((prev) => Math.max(prev - 1, min));
return (
<Input.Root
type="number"
value={value}
onChange={(e) => setValue(Number(e.target.value))}
min={min}
max={max}
>
<Input.Slot position="leading">
<Button
variant="ghost"
size="1"
icon={{ icon: Minus, alt: "Decrease" }}
onClick={decrement}
disabled={value <= min}
/>
</Input.Slot>
<Input.Slot position="trailing">
<Button
variant="ghost"
size="1"
icon={{ icon: Plus, alt: "Increase" }}
onClick={increment}
disabled={value >= max}
/>
</Input.Slot>
</Input.Root>
);
};
import { Input } from "@telegraph/input";
import { forwardRef } from "react";
// Custom textarea component
const TextAreaInput = forwardRef<HTMLTextAreaElement>((props, ref) => (
<Input.Root as="textarea" tgphRef={ref} {...props} />
));
// Usage
<TextAreaInput placeholder="Enter your message..." rows={4} />;
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { AlertCircle, CheckCircle } from "lucide-react";
import { useEffect, useState } from "react";
export const ValidatedInput = ({ validate, ...props }) => {
const [value, setValue] = useState("");
const [isValid, setIsValid] = useState<boolean | null>(null);
useEffect(() => {
if (value) {
setIsValid(validate(value));
} else {
setIsValid(null);
}
}, [value, validate]);
return (
<Input
{...props}
value={value}
onChange={(e) => setValue(e.target.value)}
errored={isValid === false}
TrailingComponent={
isValid !== null && (
<Icon
icon={isValid ? CheckCircle : AlertCircle}
color={isValid ? "green" : "red"}
alt={isValid ? "Valid" : "Invalid"}
/>
)
}
/>
);
};
// Usage
<ValidatedInput
placeholder="Enter email"
validate={(email) => /\S+@\S+\.\S+/.test(email)}
/>;
The input component uses Telegraph design tokens for consistent styling:
Container Heights:
"1": var(--tgph-spacing-6) (24px)"2": var(--tgph-spacing-8) (32px)"3": var(--tgph-spacing-10) (40px)Text Sizes:
"1": var(--tgph-text-1)"2": var(--tgph-text-2)"3": var(--tgph-text-3)Outline Variant:
var(--tgph-surface-1)var(--tgph-gray-6)var(--tgph-gray-7)var(--tgph-blue-8)Ghost Variant:
transparentvar(--tgph-gray-3)var(--tgph-gray-4)Error State:
var(--tgph-red-6)aria-invalid, aria-describedby supportaria-describedbyaria-required for required inputs// ✅ Good accessibility practices
<Stack gap="1">
<Text as="label" htmlFor="search">Search Products</Text>
<Input
id="search"
placeholder="Enter product name..."
aria-describedby={error ? "search-error" : undefined}
aria-invalid={!!error}
errored={!!error}
LeadingComponent={<Icon icon={Search} alt="Search" />}
/>
{error && (
<Text id="search-error" size="1" color="red">
{error}
</Text>
)}
</Stack>
// ❌ Poor accessibility
<Input placeholder="Search" /> {/* No label */}
<Input
LeadingComponent={<Icon icon={Search} />} {/* No alt text */}
/>
| Key | Action |
|---|---|
Tab | Focus next element |
Shift + Tab | Focus previous element |
Enter | Submit form (if in form) |
Escape | Clear input (if custom clear handler) |
htmlFor and id to associate labels with inputsaria-requiredimport { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Stack } from "@telegraph/layout";
import { Text } from "@telegraph/typography";
import { Mail, MessageSquare, User } from "lucide-react";
export const ContactForm = () => (
<Stack gap="4" as="form">
<Stack gap="1">
<Text as="label" htmlFor="name">
Full Name
</Text>
<Input
id="name"
placeholder="Enter your full name"
LeadingComponent={<Icon icon={User} alt="Name" />}
/>
</Stack>
<Stack gap="1">
<Text as="label" htmlFor="email">
Email Address
</Text>
<Input
id="email"
type="email"
placeholder="Enter your email"
LeadingComponent={<Icon icon={Mail} alt="Email" />}
/>
</Stack>
<Stack gap="1">
<Text as="label" htmlFor="message">
Message
</Text>
<Input.Root
as="textarea"
id="message"
placeholder="Enter your message"
rows={4}
>
<Input.Slot position="leading">
<Icon icon={MessageSquare} alt="Message" />
</Input.Slot>
</Input.Root>
</Stack>
<Button type="submit">Send Message</Button>
</Stack>
);
import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Filter, ScanBarcode, Search } from "lucide-react";
import { useState } from "react";
export const ProductSearch = () => {
const [searchQuery, setSearchQuery] = useState("");
return (
<Input
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
placeholder="Search products..."
size="3"
LeadingComponent={<Icon icon={Search} alt="Search" />}
TrailingComponent={
<Stack direction="row" gap="1">
<Button
variant="ghost"
size="1"
icon={{ icon: ScanBarcode, alt: "Scan barcode" }}
onClick={openBarcodeScanner}
/>
<Button
variant="ghost"
size="1"
icon={{ icon: Filter, alt: "Filter results" }}
onClick={openFilters}
/>
</Stack>
}
/>
);
};
import { Button } from "@telegraph/button";
import { Icon } from "@telegraph/icon";
import { Input } from "@telegraph/input";
import { Stack } from "@telegraph/layout";
import { Eye, EyeOff, Lock, Mail } from "lucide-react";
import { useState } from "react";
export const LoginForm = () => {
const [showPassword, setShowPassword] = useState(false);
return (
<Stack gap="4" as="form">
<Input
type="email"
placeholder="Email address"
LeadingComponent={<Icon icon={Mail} alt="Email" />}
/>
<Input
type={showPassword ? "text" : "password"}
placeholder="Password"
LeadingComponent={<Icon icon={Lock} alt="Password" />}
TrailingComponent={
<Button
variant="ghost"
size="1"
icon={{
icon: showPassword ? EyeOff : Eye,
alt: showPassword ? "Hide password" : "Show password",
}}
onClick={() => setShowPassword(!showPassword)}
/>
}
/>
<Button type="submit">Sign In</Button>
</Stack>
);
};
See our Contributing Guide for more details.
MIT License - see LICENSE for details.
FAQs
Input component for Telegraph
We found that @telegraph/input demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Security News
Axios compromise traced to social engineering, showing how attacks on maintainers can bypass controls and expose the broader software supply chain.

Security News
Node.js has paused its bug bounty program after funding ended, removing payouts for vulnerability reports but keeping its security process unchanged.

Security News
The Axios compromise shows how time-dependent dependency resolution makes exposure harder to detect and contain.