Inputs
Input
A single-line text field with optional prefix and suffix adornments, three sizes that match buttons, and an invalid state. Wrap it in Field to get a label, hint and error message that are wired up for screen readers automatically.
Letters, numbers and dashes.
That handle is already taken.
<div className="mx-auto flex w-full max-w-sm flex-col gap-5"> <Field label="Work email"> <Input type="email" placeholder="ada@fernhill.example" prefix={<Mail strokeWidth={1.75} />} /> </Field> <Field label="Workspace URL" hint="Letters, numbers and dashes."> <Input defaultValue="fernhill" prefix="https://" suffix=".plinth.app" /> </Field> <Field label="Handle" error="That handle is already taken."> <Input defaultValue="ada" prefix={<AtSign strokeWidth={1.75} />} /> </Field></div>Usage
import { Field } from "@/components/ui/field";import { Input } from "@/components/ui/input"; export function Example() { return ( <Field label="Email" hint="We'll send the receipt here."> <Input type="email" placeholder="ada@fernhill.example" /> </Field> );}Examples
Overview
Letters, numbers and dashes.
That handle is already taken.
<div className="mx-auto flex w-full max-w-sm flex-col gap-5"> <Field label="Work email"> <Input type="email" placeholder="ada@fernhill.example" prefix={<Mail strokeWidth={1.75} />} /> </Field> <Field label="Workspace URL" hint="Letters, numbers and dashes."> <Input defaultValue="fernhill" prefix="https://" suffix=".plinth.app" /> </Field> <Field label="Handle" error="That handle is already taken."> <Input defaultValue="ada" prefix={<AtSign strokeWidth={1.75} />} /> </Field></div>Sizes
32, 36 and 40px: the same heights as Button, Select and the other fields, so rows line up.
<div className="mx-auto flex w-full max-w-sm flex-col gap-3"> <Input size="sm" placeholder="Small · 32px" aria-label="Small" /> <Input size="md" placeholder="Medium · 36px" aria-label="Medium" /> <Input size="lg" placeholder="Large · 40px" aria-label="Large" /></div>Prefix and suffix
Adornments sit inside the border. Text, icons and small buttons all work; clicking an adornment focuses the input.
<div className="mx-auto flex w-full max-w-sm flex-col gap-3"> <Input type="search" placeholder="Search invoices" aria-label="Search invoices" prefix={<Search strokeWidth={1.75} />} suffix={<kbd className="rounded-ui-sm border border-line bg-raised px-1.5 font-mono text-[11px] leading-5 text-muted">⌘K</kbd>} /> <Input defaultValue="1,250.00" aria-label="Amount" prefix="$" suffix="USD" inputClassName="text-right tabular" /> <Input defaultValue="14" aria-label="Payment terms" suffix="days" inputClassName="tabular" /></div>States
Invalid comes from Field's error (or the invalid prop). Disabled and read-only use the native attributes.
Enter a valid EU VAT number.
Set by your admin.
Read-only, safe to share.
<div className="mx-auto grid w-full max-w-xl gap-5 sm:grid-cols-2"> <Field label="VAT number" error="Enter a valid EU VAT number."> <Input defaultValue="DE12345" /> </Field> <Field label="Invoice prefix" hint="Set by your admin." disabled> <Input defaultValue="FHS-" /> </Field> <Field label="Account ID" hint="Read-only, safe to share."> <Input readOnly defaultValue="acct_8Fh2kQ" inputClassName="font-mono text-[13px]" /> </Field> <Field label="Company" required> <Input placeholder="Ledgerly Ltd." /> </Field></div>Password with toggle
A suffix button that toggles visibility. Validation runs as you type and swaps the hint for an error.
At least 12 characters.
"use client"; import { useState } from "react";import { Eye, EyeOff, Lock } from "lucide-react";import { Field } from "@/components/ui/field";import { Input } from "@/components/ui/input"; export function PasswordInputDemo() { const [visible, setVisible] = useState(false); const [value, setValue] = useState("hunter-two-2026"); const tooShort = value.length > 0 && value.length < 12; return ( <div className="mx-auto w-full max-w-sm"> <Field label="Password" hint="At least 12 characters." error={tooShort ? "Use 12 characters or more." : undefined}> <Input type={visible ? "text" : "password"} value={value} onChange={(e) => setValue(e.target.value)} autoComplete="new-password" prefix={<Lock strokeWidth={1.75} />} suffix={ <button type="button" onClick={() => setVisible((v) => !v)} aria-label={visible ? "Hide password" : "Show password"} aria-pressed={visible} className="-mr-1.5 flex size-7 items-center justify-center rounded-ui-sm text-subtle hover:bg-raised hover:text-fg" > {visible ? <EyeOff strokeWidth={1.75} /> : <Eye strokeWidth={1.75} />} </button> } /> </Field> </div> );}Props
Input
size"sm" | "md" | "lg"default: "md"Height 32 / 36 / 40px.prefixReactNodedefault: —Icon or text inside the border, before the value.suffixReactNodedefault: —Icon, unit, shortcut or small button after the value.invalidbooleandefault: —Red border and aria-invalid. Set automatically by <Field error>.classNamestringdefault: —Classes for the outer box (width, margins).inputClassNamestringdefault: —Classes for the inner <input> (alignment, font)....propsInputHTMLAttributesdefault: —Any native input attribute: type, value, onChange, placeholder, autoComplete…
Field
labelrequiredReactNodedefault: —Visible label, linked to the control.hintReactNodedefault: —Help text under the control (aria-describedby).errorReactNode | booleandefault: —Error message. Replaces the hint and marks the control invalid.requiredbooleandefault: —Adds a red asterisk and the native required attribute.optionalbooleandefault: —Adds a quiet “Optional” tag after the label.disabledbooleandefault: —Disables the control inside.actionReactNodedefault: —Small element at the right of the label row, e.g. a link.htmlForstringdefault: —Id for the control. Generated when omitted.
Accessibility
- Field renders a real <label for> and gives the control its id, so clicking the label focuses the input and screen readers announce it.
- The hint or error is linked through aria-describedby; when an error is shown, the input also gets aria-invalid="true".
- Errors show an icon and text in addition to the red border, so the state never relies on color alone.
- Without a visible label (search boxes, table filters) pass aria-label on the Input.
- Adornment buttons (like the password toggle) are real buttons with an aria-label and aria-pressed.