Skip to main content

Input

An input is Headless UI's Input rendered inside a styled frame. The frame carries the border, background, size and focus ring from the control tokens; the native <input> inside is transparent. Inside a Field it receives the field's id, aria-labelledby, aria-describedby, aria-invalid and disabled state without any ids on your side.

import Input from '@zuilib/primitives/input'

Variants

outline is the default field: --input border on --background. ghost has no border until it is hovered or focused, for inline editing.

Loading example

Sizes

sm, md and lg read --control-height-*, --control-padding-x-* and the matching --text-* step, the same scale every control shares. Below the sm breakpoint (40rem) a size="sm" input types at 16px so iOS does not zoom the page on focus; from sm up it stays at 14px (md is 16px at every width).

Loading example

States

invalid colours the border and focus ring --danger and sets aria-invalid. unsaved tints the field --input-unsaved; disabled dims it and blocks the pointer.

Loading example

Leading and trailing content

leadingContent and trailingContent sit inside the frame, beside the native input, so they share its border and focus ring. Icons should be aria-hidden (the built-in ones already are).

Loading example

In a Field

Field generates the control id and wires the Label, FieldDescription and FieldError to the input. Its invalid becomes the input's default invalid, so one prop on the field paints the border and sets aria-invalid.

Loading example

Props

InputProps extends InputHTMLAttributes<HTMLInputElement> (minus the native size) with:

PropTypeDefaultDescription
variant'outline' | 'ghost''outline'
invalidbooleanField invalidaria-invalid, data-invalid and the danger styles. Inherits the enclosing Field when unset
size'sm' | 'md' | 'lg''md'
fullWidthbooleanfalsew-full on the frame
unsavedbooleanfalseTints the field --input-unsaved and sets data-unsaved; pair with Label unsaved
leadingContentReactNodeInside the frame, before the native input
trailingContentReactNodeInside the frame, after the native input
disabledbooleanField disabledLeave unset to inherit from Field / Fieldset
classNamestringMerged last onto the frame (data-slot="input")
inputClassNamestringMerged last onto the native <input>

Every other prop (type, value, onChange, placeholder, name, autoComplete, data-*, …) lands on the native <input>. Mouse and pointer enter/leave handlers are attached to the native input too, so currentTarget is the element you expect.

Slots

SlotElementNotes
[data-slot="input"]divThe frame. Carries data-unsaved and Headless UI's data-focus / data-hover / data-disabled / data-invalid
[data-slot="input-control"]inputThe native input; mirrors the same state attributes
[data-slot="input-leading"]divWraps leadingContent
[data-slot="input-trailing"]divWraps trailingContent
/* Consumer CSS: pill inputs */
[data-slot="input"] {
border-radius: var(--radius-full);
}

Tokens

TokenUsed for
--control-height-sm/md/lgFrame height per size
--control-padding-x-sm/md/lgHorizontal padding per size
--input, --background, --foreground, --muted-foregroundBorder, surface, text and placeholder
--input-unsavedunsaved tint
--ring, --dangerFocus ring; invalid state
--radius-mdCorner radius
--duration-fastColour transition

Accessibility

  • A real <input>; inside a Field it gets the field's id, aria-labelledby and aria-describedby (FieldDescription and FieldError).
  • invalid sets aria-invalid="true".
  • Focus shows a 1px ring hugging the border (focus-within), coloured --danger when invalid. Content in leadingContent / trailingContent is inside the focus ring.
  • disabled uses the native attribute, inherited from Field / Fieldset when left unset.
  • Outside a Field, pass id and use Label htmlFor, or aria-label.