Input
A text input with label, description, error state, leading/trailing icons, three sizes, and three visual variants.
AccessibleDark Mode3 Sizes3 VariantsIconsError State
Install
$
npx react-principles add input01
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 placeholder="Enter your email" /> // With label + description <Input label="Email address" description="We'll never share your email." placeholder="you@example.com" type="email" /> // Error state <Input label="Username" error="Username is already taken." defaultValue="johndoe" /> // With icons <Input label="Search" leadingIcon={<SearchIcon />} trailingIcon={<ClearIcon />} placeholder="Search..." /> // Sizes: "sm" | "md" | "lg" // Variants: "default" | "filled" | "ghost" <Input size="lg" variant="filled" label="Display name" />
Backward compatible: API lama <Input /> tetap jalan, canonical style pakai <Input />.
04
Copy-Paste (Single File)
Input.tsx
import { forwardRef, InputHTMLAttributes, ReactNode } from "react"; import { cn } from "@/lib/utils"; // ─── Types ──────────────────────────────────────────────────────────────────── export type InputSize = "sm" | "md" | "lg"; export type InputVariant = "default" | "filled" | "ghost"; export interface InputProps extends Omit<InputHTMLAttributes<HTMLInputElement>, "size"> { label?: string; description?: string; error?: string; size?: InputSize; variant?: InputVariant; leadingIcon?: ReactNode; trailingIcon?: ReactNode; } // ─── Constants ──────────────────────────────────────────────────────────────── const SIZE_CLASSES: Record<InputSize, { input: string; label: string; icon: string }> = { sm: { input: "h-8 px-3 text-xs", label: "text-xs", icon: "h-3.5 w-3.5" }, md: { input: "h-10 px-3.5 text-sm", label: "text-sm", icon: "h-4 w-4" }, lg: { input: "h-12 px-4 text-base", label: "text-sm", icon: "h-4.5 w-4.5" }, }; const VARIANT_BASE: Record<InputVariant, string> = { default: "border border-slate-200 dark:border-[#1f2937] bg-white dark:bg-[#0d1117] " + "hover:border-slate-300 dark:hover:border-slate-600 " + "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20", filled: "border border-transparent bg-slate-100 dark:bg-[#161b22] " + "hover:bg-slate-150 dark:hover:bg-[#1f2937] " + "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20 focus-within:bg-white dark:focus-within:bg-[#0d1117]", ghost: "border border-transparent bg-transparent " + "hover:bg-slate-50 dark:hover:bg-[#161b22] " + "focus-within:border-primary dark:focus-within:border-primary focus-within:ring-2 focus-within:ring-primary/20", }; const ERROR_OVERRIDE = "border-red-400 dark:border-red-500 focus-within:border-red-400 dark:focus-within:border-red-500 focus-within:ring-red-400/20"; // ─── Component ──────────────────────────────────────────────────────────────── export const Input = forwardRef<HTMLInputElement, InputProps>(function InputRoot( { label, description, error, size = "md", variant = "default", leadingIcon, trailingIcon, disabled, className, id, ...rest }, ref ) { const s = SIZE_CLASSES[size]; const inputId = id ?? (label ? label.toLowerCase().replace(/\s+/g, "-") : undefined); 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, disabled && "opacity-50" )} > {label} </label> )} <div className={cn( "relative flex items-center rounded-lg transition-all", VARIANT_BASE[variant], error && ERROR_OVERRIDE, disabled && "opacity-50 cursor-not-allowed pointer-events-none" )} > {leadingIcon && ( <span className={cn("absolute left-3 flex shrink-0 items-center text-slate-400", s.icon)}> {leadingIcon} </span> )} <input ref={ref} id={inputId} disabled={disabled} className={cn( "w-full bg-transparent outline-hidden text-slate-900 dark:text-white placeholder:text-slate-400 dark:placeholder:text-slate-500", s.input, leadingIcon && (size === "sm" ? "pl-8" : size === "lg" ? "pl-10" : "pl-9"), trailingIcon && (size === "sm" ? "pr-8" : size === "lg" ? "pr-10" : "pr-9") )} {...rest} /> {trailingIcon && ( <span className={cn("absolute right-3 flex shrink-0 items-center text-slate-400", s.icon)}> {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> ); });
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. |