Input

A styled text input with optional label, hint/error messaging, and icon addons. Supports all native input types.

Basic usage

Pass a label for accessible form fields. Without a label, ensure you provide an aria-label.

The extra props are additive: with none of label, error, hint or an addon, the component renders a single bare <input> with no wrapper — safe to drop straight into a grid cell or a flex row. Add any one of them and it wraps itself in a full-width block.

With addons

Use leftAddon or rightAddon to place icons inside the input bounds. leftIcon and rightIcon are aliases of the same two slots. Addons are decorative — they carry pointer-events-none, so for a clickable affordance such as a reveal-password toggle reach for InputGroup instead.

States

Provide feedback with hint or error. Disabled inputs reduce opacity and block interaction.

We'll never share your email.

This field is required.

Flagging invalid without a message

The invalid state has two entry points. error owns both the styling and the message; aria-invalid on its own applies the same border and focus ring but renders nothing below. Use the second form when a wrapper already prints the message — FieldError, the FormField composite, or FormMessage from @comitor/ui/form — so the error text is not duplicated.

Enter a valid email.

Border and state tokens

Every border the field can wear is a role token, never a raw scale step. That is what lets the same component stay legible in the default palette, in dark mode and under data-contrast="high" without a single palette-specific override.

  • Rest — border-input, the shared input edge.
  • Hover — border-control-edge-strong. It is deliberately a full step darker than rest, so pointing at a field makes it more visible, not less.
  • Focus — focus:ring-2 focus:ring-ring/50 plus focus:border-primary-ink. Going through the token rather than a hard-coded gold-300 is what lets the palette move it: in the default palette --primary-ink points straight at --primary (the same brand gold), and under data-contrast="high" it drops to the much darker gold-500, where a focused field clears the 3:1 non-text threshold.
  • Invalid — border-destructive-ink for the border and text-destructive-ink for the message, one token for both, with focus:ring-destructive/30 as the focus halo. destructive and red are fill roles — do not reuse them for error text or borders.
  • Disabled — disabled:bg-muted at 50% opacity, with the native attribute doing the interaction blocking.

Text size

Every field in the package sets its text with one rule, text-base md:text-sm: 16px below the md breakpoint, 14px from md up. Textarea always did. Since 3.13.4 so do Input, the triggers of Select, Combobox, MultiCombobox and DateRangePicker, TimePicker, TagsInput, Mention, MaskInput, InputOTP, Command, CommandPalette and the value field of KeyValue — and DatePicker, which is an Input. Before that they were text-sm at every width, so on a phone a 16px Textarea sat beside a 14px Input. Desktop did not change, and the fields of fixed height are still h-9. The fields that grow with their content — the MultiCombobox trigger, an empty TagsInput, a one-line KeyValue value — stay at 36px too since 3.13.5, which gives them leading-5: in 3.13.4 the 24px line of 16px text made them 38px below md. A field of your own that grows with its content needs the same leading-5, right after text-base.

  • Leave the size to the field. A text-sm of your own merges over the base step only, which keeps that one field at 14px between 16px ones on an Android phone. A field you build yourself follows the same rule — or, better, is an Input.
  • iOS gets one more guarantee. WebKit zooms the whole page when a field with text under 16px takes focus, and does not zoom back out. On iOS and iPadOS only, the package stylesheet therefore holds every typing input, textarea, select and editable surface at max(16px, 1em) (3.13.4) — yours included — which also covers the two cases the class rule misses: an iPad wider than md, and the sm font-size axis, where 1rem is 14px. So there is nothing to set in your viewport; how not to lock zoom is in Get started.

Props

PropTypeDefaultDescription
labelstring—Label text rendered above the input and linked to it with htmlFor.
errorstring—Error message rendered below. Its presence also flips the field into the invalid state.
hintstring—Helper text rendered below. Suppressed while error is set.
leftAddonReact.ReactNode—Content pinned inside the left edge of the field (icon, unit, prefix). Adds pl-9 to the input.
rightAddonReact.ReactNode—Content pinned inside the right edge of the field. Adds pr-9 to the input.
leftIconReact.ReactNode—Alias of leftAddon. leftAddon wins when both are passed.
rightIconReact.ReactNode—Alias of rightAddon. rightAddon wins when both are passed.
aria-invalidboolean | 'true' | 'false'—Flips the field into the invalid state without rendering a message — for when the error text is owned by a Field or a Form.
idstringReact.useId()Falls back to a generated id, which is what wires the label and the hint/error to the input.
typestring'text'Any native input type — email, password, number, date, search…
disabledbooleanfalseBlocks interaction, drops the field to 50% opacity and swaps the fill to muted.
classNamestring—Additional Tailwind classes merged onto the <input> element itself, not the wrapper.
...restReact.ComponentProps<'input'>—All native input props are forwarded.

Accessibility

  • Label is automatically associated via htmlFor, using either the id you pass or a generated one.
  • aria-invalid is set when error is provided, and passing aria-invalid yourself produces the same styling.
  • Hint and error messages are linked via aria-describedby; the error wins when both are present, so the description never points at stale help text.
  • Focus draws a gold ring (--ring) and swaps the border to --primary-ink, so the field keeps a visible boundary at focus rather than dissolving into the page.
  • Hover strengthens the border (--control-edge-strong) instead of merely tinting it. In the default palette that is still a light grey step, so hover is an affordance hint, not the accessible boundary — the 3:1 non-text threshold is met by the high-contrast palette, which raises the same token to a genuinely dark edge.