Input
A text input with label, description, error state, leading/trailing icons, three sizes, and three visual variants.
AccessibleDark Mode3 Sizes3 VariantsIconsError State
01
Theme Preview
Normal, focused, error, and disabled states — rendered with forced light and dark styling.
Light
Email address
you@example.com
Username
johndoe_xyz
Password
••••••••
Password must be at least 8 characters.
Display name
John Doe
Dark
Email address
you@example.com
Username
johndoe_xyz
Password
••••••••
Password must be at least 8 characters.
Display name
John Doe
02
Live Demo
Size
Variant
We'll never share your email.
Username is already taken.
03
Code Snippet
src/ui/Input.tsx
import { Input } from "@/ui/Input"; // Basic <Input.Root placeholder="Enter your email" /> // With label + description <Input.Root label="Email address" description="We'll never share your email." placeholder="you@example.com" type="email" /> // Error state <Input.Root label="Username" error="Username is already taken." defaultValue="johndoe" /> // With icons <Input.Root label="Search" leadingIcon={<SearchIcon />} trailingIcon={<ClearIcon />} placeholder="Search..." /> // Sizes: "sm" | "md" | "lg" // Variants: "default" | "filled" | "ghost" <Input.Root size="lg" variant="filled" label="Display name" />
Backward compatible: API lama <Input /> tetap jalan, canonical style pakai <Input.Root />.
04
Copy-Paste (Single File)
Input.tsx
import { forwardRef, type InputHTMLAttributes, type ReactNode } from "react"; type InputSize = "sm" | "md" | "lg"; type InputVariant = "default" | "filled" | "ghost"; interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> { label?: string; description?: string; error?: string; size?: InputSize; variant?: InputVariant; leadingIcon?: ReactNode; trailingIcon?: ReactNode; } const cn = (...classes: Array<string | undefined | false>) => classes.filter(Boolean).join(" "); const SIZE_CLASSES = { sm: { input: "h-8 px-3 text-xs", label: "text-xs" }, md: { input: "h-10 px-3.5 text-sm", label: "text-sm" }, lg: { input: "h-12 px-4 text-base", label: "text-sm" }, } as const; const VARIANT_CLASSES = { default: "border border-slate-200 bg-white dark:border-[#1f2937] dark:bg-[#0d1117]", filled: "border border-transparent bg-slate-100 dark:bg-[#161b22]", ghost: "border border-transparent bg-transparent", } as const; const InputRoot = forwardRef<HTMLInputElement, InputProps>(function InputRoot( { label, description, error, size = "md", variant = "default", leadingIcon, trailingIcon, className, id, ...props }, ref ) { const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined); const s = SIZE_CLASSES[size]; return ( <div className={cn("flex flex-col gap-1.5", className)}> {label && <label htmlFor={inputId} className={cn("font-medium text-slate-700 dark:text-slate-300", s.label)}>{label}</label>} <div className={cn("relative flex items-center rounded-lg transition-all focus-within:ring-2 focus-within:ring-primary/20", VARIANT_CLASSES[variant], error && "border-red-400 dark:border-red-500")}> {leadingIcon && <span className="absolute left-3 text-slate-400">{leadingIcon}</span>} <input ref={ref} id={inputId} className={cn("w-full bg-transparent text-slate-900 outline-hidden placeholder:text-slate-400 dark:text-white dark:placeholder:text-slate-500", s.input, leadingIcon && "pl-9", trailingIcon && "pr-9")} {...props} /> {trailingIcon && <span className="absolute right-3 text-slate-400">{trailingIcon}</span>} </div> {description && !error && <p className="text-xs text-slate-500 dark:text-slate-400">{description}</p>} {error && <p className="text-xs text-red-500 dark:text-red-400">{error}</p>} </div> ); }); type InputCompound = typeof InputRoot & { Root: typeof InputRoot }; export const Input = Object.assign(InputRoot, { Root: InputRoot }) as InputCompound;
05
Props
Extends all native HTMLInputElement attributes.
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label rendered above the input. |
description | string | — | Helper text shown below the input (hidden when error is present). |
error | string | — | Error message — turns border red and replaces description. |
size | "sm" | "md" | "lg" | "md" | Controls input height and text size. |
variant | "default" | "filled" | "ghost" | "default" | Visual style of the input wrapper. |
leadingIcon | ReactNode | — | Icon placed at the left edge inside the input. |
trailingIcon | ReactNode | — | Icon placed at the right edge inside the input. |
disabled | boolean | false | Disables interaction and reduces opacity. |