Components

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.

Props

PropTypeDefaultDescription
labelstringLabel text rendered above the input and linked to it with htmlFor.
errorstringError message rendered below. Its presence also flips the field into the invalid state.
hintstringHelper text rendered below. Suppressed while error is set.
leftAddonReact.ReactNodeContent pinned inside the left edge of the field (icon, unit, prefix). Adds pl-9 to the input.
rightAddonReact.ReactNodeContent pinned inside the right edge of the field. Adds pr-9 to the input.
leftIconReact.ReactNodeAlias of leftAddon. leftAddon wins when both are passed.
rightIconReact.ReactNodeAlias 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.
classNamestringAdditional 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.