GitHub

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.

PropTypeDefaultDescription
labelstringLabel rendered above the input.
descriptionstringHelper text shown below the input (hidden when error is present).
errorstringError 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.
leadingIconReactNodeIcon placed at the left edge inside the input.
trailingIconReactNodeIcon placed at the right edge inside the input.
disabledbooleanfalseDisables interaction and reduces opacity.
react-principles