Input
A single-line text field: 32px, the input-tint outline, a louder edge on hover and the brand ring on focus. Text is 16px on a phone so iOS never zooms, 14px from md. Source: src/components/ui/input.tsx.
Default
Always pair an input with a visible Label. A hint underneath is linked with aria-describedby so screen readers read it after the label.
Shown in the sidebar and in shared links.
Types
The native type does the work: the right keyboard on a phone, autofill, the password mask, number steppers and the file picker.
Variants
Two. Default draws the box; bare removes it so the input can live inside a shell you draw, keeping only the text styles and placeholder tier.
Need a prefix icon, a unit or an inline button inside the field? Use Input group rather than hand-building a bare shell.
States
Hover lifts the outline, focus adds the 3px brand ring, disabled fades to 50% on a tint fill. aria-invalid swaps both outline and ring to destructive.
Read-only has no style of its own; it looks like a normal field but cannot be edited. There is no loading state.
Validation
The system has no Field or error-message primitive yet. The input styles aria-invalid; the label, message and their wiring are composed by hand, as below. Try submitting empty, then type.
Validate on blur and submit
Character limit
With a button
An inline form: the input fills the row and a sm Button (32px, the same height) submits it. A visually hidden label is acceptable only when the placeholder and button make the purpose obvious.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| variant | "default" | "bare" | "default" | bare drops height, padding, border, radius and the focus ring for a field that sits inside another shell (a menu search row, the composer). Mirrored as data-variant. |
| type | HTMLInputTypeAttribute | — | Passed to the native input. file inputs get a styled 24px file button. |
| aria-invalid | boolean | "true" | "false" | — | Turns the outline destructive with a 3px destructive ring. The message is yours to render. |
| ...props | ComponentProps<"input"> | — | Native attributes pass through (disabled, readOnly, placeholder, value / onChange). className merges last. There is no size prop: every input is 32px tall. |
Best practices
When to use
- One line of free text: names, emails, URLs, numbers. Use Textarea for anything that wraps.
- Picking from a known list is a Select, not an input with validation.
- Prefixes, suffixes and inline actions belong in Input group.
Behavior
- Validate on blur or submit, never on every keystroke while the person is still typing.
- Once a field is invalid, clear the error as soon as the value becomes valid.
- Set type and autoComplete so phones show the right keyboard and autofill works.
Accessibility
- Every input has a Label with a matching htmlFor, or an aria-label when the label is truly redundant.
- Link hints and errors with aria-describedby; set aria-invalid only after the person has interacted.
- Placeholder is an example, not a label: it disappears on input and sits on the third text tier.
Content
- Labels are nouns in sentence case: Work email, not EMAIL ADDRESS:.
- Errors say what is wrong and how to fix it: That doesn't look like an email. Check for an @.