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 FormItem 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/components/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.

Loading example

States

state colours the border and the focus ring: error uses --destructive and also marks the control invalid, success uses --success. unsaved tints the field --input-dirty; 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 FormItem

FormItem generates the control id and wires the Label, Description and Message to the input. Its invalid becomes the input's default invalid, so one prop on the item paints the border and sets aria-invalid.

Loading example

Props

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

PropTypeDefaultDescription
variant'outline' | 'ghost''outline'
state'default' | 'error' | 'success''default'Border and ring colour; error also marks the control invalid. Exposed as data-state
invalidbooleanFormItem invalidaria-invalid, data-invalid and the error styles. Inherits the enclosing FormItem when unset
size'sm' | 'md' | 'lg''md'
fullWidthbooleanfalsew-full on the frame
unsavedbooleanfalseTints the field --input-dirty and sets data-unsaved; pair with Label isDirty
leadingContentReactNodeInside the frame, before the native input
trailingContentReactNodeInside the frame, after the native input
disabledbooleanFormItem disabledLeave unset to inherit from FormItem / 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-state, 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-dirtyunsaved tint
--ring, --destructive, --successFocus ring per state
--radius-mdCorner radius
--duration-fastColour transition

Accessibility

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