Skip to content

Input

A single-line text field — with optional in-field leading/trailing slots and built-in clear, password-reveal, and loading affordances.

Stablev0.4.2added in v0.1.0@garn/ui/input
On 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.

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.

terminal
npx garn-ui add input
First time? Set up garn in your project
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

SelectorState
:hoverhover
:focusfocus
:focus-visiblefocus-visible
[aria-invalid=true]invalid
[aria-busy=true]loading
[disabled]disabled
::placeholderplaceholder
PropertyTokenTier
height--garn-control-h-mdsemantic · air
paddingInline--garn-control-px-mdsemantic · air
borderColor--garn-foregroundsemantic
iconInset--garn-control-icon-insetsemantic

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

Do
  • Pair every input with a label (visible <label htmlFor> preferred).
  • Set the right type (email, password, search, number…) so the keyboard and validation fit; pair revealable with type=password and the right autoComplete.
  • Use leading/trailing for in-field icons/units; reach for the built-in clearable / loading rather than hand-building affordances.
  • Reflect validation with aria-invalid + a described error message.
Don't
  • 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

warn

Give every input an accessible label (a <label htmlFor>, or aria-label/aria-labelledby).

when no associated <label htmlFor>, aria-label, or aria-labelledby

A1.3.1Info and RelationshipsA2.1.1KeyboardAA2.4.7Focus VisibleA4.1.2Name, Role, ValueAA4.1.3Status Messages
  • 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.