Trang này chưa có bản tiếng Việt. Nội dung dưới đây là bản tiếng Anh.
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.
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-smof 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,selectand editable surface atmax(16px, 1em)(3.13.4) — yours included — which also covers the two cases the class rule misses: an iPad wider thanmd, and thesmfont-size axis, where1remis 14px. So there is nothing to set in yourviewport; how not to lock zoom is in Get started.
Props
| Prop | Kiểu | Mặc định | Mô tả |
|---|---|---|---|
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.