Input
A single-line text field — with optional in-field leading/trailing slots and built-in clear, password-reveal, and loading affordances.
@garn/ui/inputOn this page
Default
Default text input.
Appearance
The shared field-surface fill axis (see lib/field-variants.ts): outline = a semi-transparent neutral border on a transparent fill (the default); soft = a subtle neutral fill with no edge that clears to the full outline on focus; ghost = seamless (no fill / edge) until focus — for inline / table-cell fields.
Outline
Soft
Ghost
Sizing
Control height rung off the locked --garn-control-* ladder (24·28·32·40·44px at default density).
Spacing
The same control rendered across garn's density modes — compact, default, and spacious.
States
Interactive states, toggled live.
Labeled
Input with an associated label.
Slots
Leading icon, trailing unit, and an interactive trailing control.
Search
Leading search icon + clearable.
Clearable
A clear button (Escape clears, focus retained).
Password
A password field with a reveal toggle.
Loading
A spinner, and a cancellable loading field.
Invalid
aria-invalid — danger edge + auto error glyph.
Installation
garn is copy-in — the garn CLI writes this component's source into your project, so you own and can edit it. It resolves any base files and installs the npm dependencies below for you.
npx garn-ui add input- Registry
input- Deps
class-variance-authoritylucide-react- Registry deps
utilsfield-variantsfield-surfacespinner
Import
import { Input } from "@garn/ui/input";Props
The component's public props and their types.
appearance- Description
- No description.
- Type
"ghost" | "outline" | "soft"- Default
outline
size- Description
- No description.
- Type
"lg" | "md" | "sm" | "xl" | "xs"- Default
md
cancelLabel- Description
- Accessible name for the cancel button.
- Type
string- Default
"Cancel"
clearable- Description
- Show a clear button when the field is non-empty; Escape also clears.
- Type
boolean- Default
false
clearLabel- Description
- Accessible name for the clear button.
- Type
string- Default
"Clear"
hidePasswordLabel- Description
- Accessible name for the reveal toggle when the password is visible.
- Type
string- Default
"Hide password"
leading- Description
- Decorative or interactive content rendered inside the field, before the text.
- Type
React.ReactNode
loading- Description
- Show a trailing spinner and mark the field `aria-busy`.
- Type
boolean- Default
false
onCancel- Description
- When loading, make the spinner a cancel button (spinner → ✕ on hover/focus) that calls this.
- Type
() => void
onClear- Description
- Fires after the field is cleared (button or Escape).
- Type
() => void
onRevealedChange- Description
- Fires when the reveal toggle flips (controlled or uncontrolled).
- Type
(revealed: boolean) => void
revealable- Description
- For a password field, render a show/hide toggle that swaps the input type.
- Type
boolean- Default
false
revealed- Description
- Controlled reveal state (pair with `onRevealedChange`); omit for uncontrolled.
- Type
boolean
showPasswordLabel- Description
- Accessible name for the reveal toggle when the password is hidden.
- Type
string- Default
"Show password"
trailing- Description
- Decorative or interactive content rendered inside the field, at the trailing edge (after the built-in affordances).
- Type
React.ReactNode
Plus 309 inherited native <input> attributes.
Styling
Target these data-slots and states, and remap these tokens, to restyle without forking the component.
[data-slot="input"][data-slot="input-root"][data-slot="input-leading"][data-slot="input-trailing"][data-slot="input-error-icon"][data-slot="input-reveal"][data-slot="input-clear"][data-slot="input-loading"][data-slot="input-cancel"]States
| Selector | State | Description |
|---|---|---|
:hover | hover | Edge / fill grows slightly stronger. |
:focus | focus | Border grows less transparent (foreground/40) on any focus — click or keyboard; soft/ghost clear their fill to the full outline. |
:focus-visible | focus-visible | Keyboard focus adds a subtle neutral ring (the accessible focus indicator). |
[aria-invalid=true] | invalid | Danger border (+ danger ring on focus) and an auto error glyph in the trailing zone. |
[aria-busy=true] | loading | A trailing spinner; with onCancel it becomes a cancel button on hover/focus. |
[disabled] | disabled | Dims and blocks pointer/typing; affordance buttons disable too. |
::placeholder | placeholder | Muted-foreground placeholder text. |
| Property | Token | Tier |
|---|---|---|
height | --garn-control-h-md | semantic · air |
paddingInline | --garn-control-px-md | semantic · air |
borderColor | --garn-foreground | semantic |
iconInset | --garn-control-icon-inset | semantic |
Private instance vars (never externalize): --in-px
When to use
Collect a single line of freeform text — name, email, search query, etc. Add a leading/trailing slot for an icon or unit, or the built-in clearable / password-reveal / loading affordances.
Reach for something else when
- Multi-line text (use a textarea)
- choosing among fixed options (use select or radio-group)
- a boolean (use checkbox or switch)
- removable/selectable chips (use Tag).
Overview
Input is the single-line text field — the most-used control in any form. In garn it's not a bare <input>: it renders the shared field surface (the same appearance × size system as Select, Textarea, and Combobox), so every text control in a form lines up to the same height, radius, and focus ring at a given density. The appearance axis sets how much the field asserts itself — outline for the default bordered field, soft for a filled rest state, ghost for a borderless field that only reveals its edge on interaction.
Beyond the bare field, Input ships the affordances you'd otherwise re-build every time: leading/trailing slots for an icon or unit, a built-in clear button, a password reveal toggle, a loading spinner, and an error treatment that reddens the border and ring when aria-invalid is set. Reach past Input only when the data isn't a single line of text — a long message wants Textarea, a fixed-length code wants InputOTP, a choice wants Select or Combobox.
Guidelines
Always pair an Input with a Label. A placeholder is not a label — it vanishes on focus and fails screen readers. Use the label component (or Form's wiring) so clicking the text focuses the field and the accessible name is announced. Reserve the placeholder for an example of the expected value, not the field's name.
Let the field surface carry sizing — don't hand-pad. Pick a size rung and the control-height tokens handle the rest; adding px-*/h-* utilities breaks alignment with the sibling controls and the density modes. If a field looks too tall or short next to its neighbors, it's almost always a stray height utility, not the token.
Signal validity through state, not color alone. Set aria-invalid on error so the built-in red border + ring and the error-icon slot engage together, and wire the message via aria-describedby (Form does this for you). Use leading/trailing for units and adornments ($, %, a search icon) rather than baking them into the value.
Best practices
- Pair every input with a label (visible <label htmlFor> preferred).
- Set the right
type(email, password, search, number…) so the keyboard and validation fit; pairrevealablewith type=password and the right autoComplete. - Use
leading/trailingfor in-field icons/units; reach for the built-inclearable/loadingrather than hand-building affordances. - Reflect validation with aria-invalid + a described error message.
- Don't rely on the placeholder as the label — it disappears on focus.
- Don't hand-set height/padding; the size variant + --garn-control-* tokens own the grid.
- Don't use leading/trailing for outside-attached addons with merged borders (a https:// cell, a flanking Select) — that's a future InputGroup.
Content guidelines
- Label the field with a short noun phrase in sentence case (“Email address”).
- Use the placeholder only for a format hint or example (“name@company.com”), never as the label.
- Keep helper text to one line, and write errors as specific, fixable instructions (“Enter a valid email”).
Troubleshooting
Typing does nothing / React warns about a controlled input.
Cause. Passing value without onChange makes the input read-only.
Fix. Use defaultValue for uncontrolled, or pass both value and onChange.
The password reveal toggle never appears.
Cause. revealable only renders the toggle for a password field.
Fix. Set type="password" alongside revealable.
The clear button never shows.
Cause. Clear is gated by clearable AND a non-empty value (and not while disabled/readOnly/loading).
Fix. Pass clearable and ensure the field has a value (controlled or uncontrolled).
The loading spinner isn't cancellable.
Cause. Cancel is derived from the onCancel handler.
Fix. Pass onCancel with loading; the spinner becomes a ✕ on field hover / keyboard focus.
className doesn't style the border / fill on an enriched field.
Cause. When the field is enriched it renders a wrapper; className styles that wrapper (the visible control), while ref + native props target the inner <input>.
Fix. Expected — style the control via className; reach the input via ref / native props.
Accessibility
- Role
textbox- Focus
- Border darkens (foreground/40) on any focus; keyboard focus adds a 2px neutral ring (foreground/15). Affordance buttons carry their own focus ring.
Accessibility requirements
Give every input an accessible label (a <label htmlFor>, or aria-label/aria-labelledby).
when no associated <label htmlFor>, aria-label, or aria-labelledby
- Associate a visible <label> (htmlFor) or provide aria-label / aria-labelledby — the placeholder is not a label.
- Reflect validation with aria-invalid (+ an aria-describedby error message); the auto error glyph is decorative (aria-hidden).
- The clear button is a real <button> with an accessible name ('Clear', via clearLabel); it is keyboard-reachable, clears + keeps focus in the field, and Escape clears too.
- The password reveal toggle is a <button aria-controls> with a Show/Hide label swap (not aria-pressed); a visually-hidden aria-live=polite region announces the state, and the type is forced back to password on form submit.
- Loading sets aria-busy on the field; the spinner is aria-hidden. With onCancel the cancel button is named (cancelLabel) and keyboard-operable.
- Leading/trailing slots keep the field a single click target — clicking empty padding or a decorative icon focuses the input, while a real button/link inside a slot still works.