Input
One line of text. A squircle field of the house glass, in three sizes that line up with Button, with an optional leading icon and an invalid state. Built on the vendored Base UI input; every native attribute passes through.
Playground
import { Input } from "@/components/shared/input";
import { SearchIcon } from "@hugeicons/core-free-icons";
<Input
leadingIcon={SearchIcon}
placeholder="Search clients"
aria-label="Search clients"
/>Sizes
sm 32px, md 36px by default, lg 40px — each the height of the Button of the same size, so a field and its action share one line.
import { Input } from "@/components/shared/input";
import { Button } from "@/components/shared/button";
<Input size="sm" placeholder="Staff name" aria-label="Staff name" />
<Button size="sm" variant="primary">Add</Button>
<Input placeholder="Staff name" aria-label="Staff name" />
<Button variant="primary">Add</Button>
<Input size="lg" placeholder="Staff name" aria-label="Staff name" />
<Button size="lg" variant="primary">Add</Button>With an icon
leadingIcon draws a decorative icon where the text starts. Use one that says what goes in — search, email, phone — and never instead of a label.
import { Input } from "@/components/shared/input";
import { Mail01Icon, Call02Icon, SearchIcon } from "@hugeicons/core-free-icons";
<Input leadingIcon={SearchIcon} placeholder="Search clients" aria-label="Search clients" />
<Input leadingIcon={Mail01Icon} type="email" placeholder="jana@studio-lumen.sk" aria-label="Email" />
<Input leadingIcon={Call02Icon} type="tel" placeholder="+421 900 123 456" aria-label="Phone" />With a badge at the end
trailing holds something short inside the field at its end, the text clearing it: a badge that says what the value is, as the owner's name on setup's TeamCard. It isn't a control — taps go to the field.
import { Badge } from "@/components/shared/badge";
import { Input } from "@/components/shared/input";
<Input
size="sm"
aria-label="Your name"
defaultValue="Mila"
trailing={<Badge variant="muted" size="sm">Owner</Badge>}
/>Types
The native type picks the phone keyboard and the browser's own pickers. Pair it with autoComplete for anything about the person, and give short values a width with className.
import { Input } from "@/components/shared/input";
<Input id="bk-email" type="email" autoComplete="email" />
<Input id="bk-phone" type="tel" autoComplete="tel" />
<Input id="sv-duration" type="number" min={5} step={5} defaultValue={30} className="w-24" />
<Input id="day-1-open" type="time" defaultValue="09:00" className="w-28" />Invalid, disabled & read-only
Invalid is aria-invalid: a destructive edge, always with a FieldError that says what's wrong, linked by aria-describedby — never a red border alone. Disabled dims the field; read-only keeps it legible and selectable.
import { Input } from "@/components/shared/input";
import { Field, FieldError, FieldLabel } from "@/components/shared/field";
<Field>
<FieldLabel htmlFor="bk-phone">Phone</FieldLabel>
<Input id="bk-phone" type="tel" defaultValue="0900" aria-invalid aria-describedby="bk-phone-error" />
<FieldError id="bk-phone-error">Enter the full number, with the area code.</FieldError>
</Field>
<Input defaultValue="Skin fade" aria-label="Service" disabled />
<Input defaultValue="barber-kamzik" aria-label="Slug" readOnly />API reference
| Prop | Type | Default | Description |
|---|---|---|---|
| size | "sm" | "md" | "lg" | "md" | 32, 36 or 40px tall — the height of the Button of the same size; on a touch screen sm and md are 40px, as Button's are. |
| leadingIcon | Icon | — | An SVG icon component drawn inside the field, before the text. Decorative: hidden from assistive tech. |
| trailing | ReactNode | — | Held inside the field at its end, the text clearing it: a short badge (Owner). Not a control. |
| aria-invalid | boolean | false | Marks the value wrong: a destructive edge. Pair with a FieldError and aria-describedby. |
| disabled | boolean | false | Disables and dims the field. |
| className | string | — | Layout only — width, margins. Lands on the outermost element: the input, or its wrapper when there's an icon. |
Everything else passes through to the <input>: id, name, type, value, onChange, autoComplete, readOnly, ref, … The native size attribute is replaced by the size above.
Guidelines
- Every input has a label: a FieldLabel whose
htmlFormatches itsid, or anaria-labelwhen a nearby heading already says what it is. A placeholder is an example (“jana@studio-lumen.sk”), never the label. - Show a problem in words. Set
aria-invalidonly once there's a FieldError to pointaria-describedbyat, and clear both when the value is fixed. - Use the right
typeandautoComplete(email,tel,name) — they pick the phone keyboard and let the browser fill the form in one tap. - Match sizes along a row: an
smfield sits beside ansmButton. Text stays 16px on phones so iOS doesn't zoom in on focus. - Size short values to their content — a duration, a time, a price — with a width in
className; let names and addresses fill the column.