Input
Text, search, password and URL inputs with icons, clear button and a labelled Field wrapper.
$
pnpm dlx shadcn@latest add https://lodeui.com/r/input.jsonEnter a valid email address.
.com
components/examples/input-demo.tsx
"use client";
import * as React from "react";
import { AtSign, Globe } from "lucide-react";
import { Field, Input, PasswordInput, SearchInput } from "@/components/ui/input";
export default function InputDemo() {
const [q, setQ] = React.useState("");
const [email, setEmail] = React.useState("ada@");
const valid = /^\S+@\S+\.\S+$/.test(email);
return (
<div className="w-full max-w-sm space-y-4">
<SearchInput value={q} onValueChange={setQ} placeholder="Search projects" aria-label="Search projects" />
<Field label="Email" htmlFor="demo-email" error={email && !valid ? "Enter a valid email address." : undefined} description="We'll never share it.">
<Input id="demo-email" type="email" leading={<AtSign />} value={email} onValueChange={setEmail} invalid={!!email && !valid} />
</Field>
<Field label="Password" htmlFor="demo-pass" required>
<PasswordInput id="demo-pass" defaultValue="correct-horse" />
</Field>
<Field label="Website" htmlFor="demo-url">
<Input id="demo-url" leading={<Globe />} trailing={<span className="text-xs">.com</span>} placeholder="northwind" />
</Field>
</div>
);
}When to use
- Single-line text, email, URL, numbers with units and search fields.
- Fields that need an icon, unit, clear button or show/hide password toggle inside.
When not to use
- Multi-line text: use a textarea.
- Choosing from known values: use a select or combobox.
- Small counts: use a number field.
Installation
$
pnpm dlx shadcn@latest add https://lodeui.com/r/input.json1Install the dependencies
npm install lucide-react2Copy the source into your project
components/ui/input.tsx"use client"; import * as React from "react"; import { Eye, EyeOff, Search, X } from "lucide-react"; import { cn } from "@/lib/utils"; export interface InputProps extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "size" | "prefix"> { /** Icon or text shown inside, before the value. */ leading?: React.ReactNode; /** Icon, unit or action shown inside, after the value. */ trailing?: React.ReactNode; invalid?: boolean; size?: "sm" | "md" | "lg"; /** Shows a clear button when there's a value. Requires `value` and `onValueChange`. */ clearable?: boolean; onValueChange?: (value: string) => void; } const heights = { sm: "h-8 text-[13px]", md: "h-10 text-sm", lg: "h-12 text-[15px]" }; export const Input = React.forwardRef<HTMLInputElement, InputProps>(function Input( { leading, trailing, invalid, size = "md", clearable, onValueChange, onChange, className, disabled, ...props }, ref, ) { const hasValue = props.value !== undefined && String(props.value).length > 0; return ( <div data-invalid={invalid || undefined} className={cn( "group flex w-full items-center gap-2 rounded-md border border-input bg-transparent px-3 transition-[border-color,box-shadow] duration-150", "focus-within:border-foreground/40 focus-within:ring-2 focus-within:ring-ring/40", "data-[invalid]:border-destructive data-[invalid]:focus-within:ring-destructive/30", disabled && "cursor-not-allowed opacity-50", heights[size], className, )} > {leading && <span className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4">{leading}</span>} <input ref={ref} disabled={disabled} aria-invalid={invalid || undefined} onChange={(e) => { onChange?.(e); onValueChange?.(e.target.value); }} className="h-full min-w-0 flex-1 bg-transparent outline-none placeholder:text-muted-foreground/70" {...props} /> {clearable && hasValue && ( <button type="button" aria-label="Clear" onClick={() => onValueChange?.("")} className="grid size-5 shrink-0 place-items-center rounded text-muted-foreground hover:bg-accent hover:text-foreground"> <X className="size-3.5" /> </button> )} {trailing && <span className="flex shrink-0 items-center text-muted-foreground [&_svg]:size-4">{trailing}</span>} </div> ); }); export const SearchInput = React.forwardRef<HTMLInputElement, Omit<InputProps, "leading" | "type">>(function SearchInput(props, ref) { return <Input ref={ref} type="search" leading={<Search />} clearable placeholder="Search" {...props} />; }); /** Password field with a show/hide toggle that keeps focus in the field. */ export const PasswordInput = React.forwardRef<HTMLInputElement, Omit<InputProps, "type" | "trailing">>(function PasswordInput(props, ref) { const [shown, setShown] = React.useState(false); return ( <Input ref={ref} type={shown ? "text" : "password"} autoComplete="current-password" trailing={ <button type="button" aria-label={shown ? "Hide password" : "Show password"} aria-pressed={shown} onMouseDown={(e) => e.preventDefault()} onClick={() => setShown((s) => !s)} className="-mr-1 grid size-7 place-items-center rounded hover:bg-accent hover:text-foreground" > {shown ? <EyeOff /> : <Eye />} </button> } {...props} /> ); }); export interface FieldProps { label: string; htmlFor?: string; description?: React.ReactNode; error?: React.ReactNode; required?: boolean; children: React.ReactNode; className?: string; } /** Label, control, help text and error, wired together for screen readers via ids. */ export function Field({ label, htmlFor, description, error, required, children, className }: FieldProps) { const id = React.useId(); const descId = description ? `${id}-desc` : undefined; const errId = error ? `${id}-err` : undefined; const control = React.isValidElement<{ "aria-describedby"?: string }>(children) ? React.cloneElement(children, { "aria-describedby": [descId, errId].filter(Boolean).join(" ") || undefined }) : children; return ( <div className={cn("space-y-1.5", className)}> <label htmlFor={htmlFor} className="flex items-center gap-1 text-sm font-medium"> {label} {required && <span className="text-destructive" aria-hidden>*</span>} </label> {control} {description && !error && <p id={descId} className="text-xs text-muted-foreground">{description}</p>} {error && <p id={errId} role="alert" className="text-xs text-destructive">{error}</p>} </div> ); }
Usage
Import it from @/components/ui/input after installing.
example.tsx
import { Field, Input, PasswordInput, SearchInput } from "@/components/ui/input";
export function SignUpFields({ emailError }: { emailError?: string }) {
return (
<div className="space-y-4">
<Field label="Work email" htmlFor="email" error={emailError} required>
<Input id="email" type="email" autoComplete="email" invalid={!!emailError} />
</Field>
<Field label="Password" htmlFor="password" description="At least 12 characters.">
<PasswordInput id="password" autoComplete="new-password" />
</Field>
<SearchInput placeholder="Search projects" aria-label="Search projects" />
</div>
);
}API reference
Input
A styled text input with slots inside the border. All native input attributes pass through, and the ref reaches the <input>.
| Prop | Type | Default | Description |
|---|---|---|---|
| leading / trailing | ReactNode | – | Icon, prefix, unit or action shown inside the field. |
| size | "sm" | "md" | "lg" | "md" | Field height. |
| invalid | boolean | – | Red border and aria-invalid. |
| clearable | boolean | – | Shows a clear button when there's a value. Needs value and onValueChange. |
| onValueChange | (value: string) => void | – | Called with the new string; onChange still works too. |
SearchInput
An Input with a search icon. Takes every Input prop except leading and type.
No props.
PasswordInput
An Input with a show/hide toggle. The toggle uses aria-pressed.
No props.
Field
Label, description and error wrapper that links them to the control with aria-describedby.
| Prop | Type | Default | Description |
|---|---|---|---|
| label | string | – | Visible label. |
| htmlFor | string | – | The id of the control. |
| description / error | ReactNode | – | Help text and an error, announced with role="alert". |
| required | boolean | – | Adds a required marker. |
Keyboard interactions
| Keys | Action |
|---|---|
| Tab | Move into and out of the field |
| Esc | Native clear in search fields |
Accessibility
- Always label the field: Field, a <label>, or aria-label.
- Errors are linked with aria-describedby and announced with role="alert".
- The password toggle is a labelled button with aria-pressed.
Motion
- Border and ring colors transition on focus (150ms).
Responsive behavior
- Fills its container width.
- Use type, inputMode and autoComplete so phones show the right keyboard.
Performance
- Plain input; no extra renders unless clearable is used.
Notes for AI
- Use Field for labelled form controls instead of hand-writing label markup.
- Set autoComplete on auth fields.
This page as Markdown, for assistants and coding agents:
/components/input/markdownThe full library index for assistants is at /llms.txt.
Lode222 doc pagesllms.txt