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/50plusfocus:border-primary-ink. Going through the token rather than a hard-codedgold-300is what lets the palette move it: in the default palette--primary-inkpoints straight at--primary(the same brand gold), and underdata-contrast="high"it drops to the much darkergold-500, where a focused field clears the 3:1 non-text threshold. - Invalid —
border-destructive-inkfor the border andtext-destructive-inkfor the message, one token for both, withfocus:ring-destructive/30as the focus halo.destructiveandredare fill roles — do not reuse them for error text or borders. - Disabled —
disabled:bg-mutedat 50% opacity, with the native attribute doing the interaction blocking.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
label | string | — | Label text rendered above the input and linked to it with htmlFor. |
error | string | — | Error message rendered below. Its presence also flips the field into the invalid state. |
hint | string | — | Helper text rendered below. Suppressed while error is set. |
leftAddon | React.ReactNode | — | Content pinned inside the left edge of the field (icon, unit, prefix). Adds pl-9 to the input. |
rightAddon | React.ReactNode | — | Content pinned inside the right edge of the field. Adds pr-9 to the input. |
leftIcon | React.ReactNode | — | Alias of leftAddon. leftAddon wins when both are passed. |
rightIcon | React.ReactNode | — | Alias of rightAddon. rightAddon wins when both are passed. |
aria-invalid | boolean | '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. |
id | string | React.useId() | Falls back to a generated id, which is what wires the label and the hint/error to the input. |
type | string | 'text' | Any native input type — email, password, number, date, search… |
disabled | boolean | false | Blocks interaction, drops the field to 50% opacity and swaps the fill to muted. |
className | string | — | Additional Tailwind classes merged onto the <input> element itself, not the wrapper. |
...rest | React.ComponentProps<'input'> | — | All native input props are forwarded. |
Accessibility
- Label is automatically associated via
htmlFor, using either theidyou pass or a generated one. aria-invalidis set whenerroris provided, and passingaria-invalidyourself 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.