Overview
<Input /> is the canonical single-line text-input primitive. It pairs with <label> for the
accessible name and proves the shared form-control recipe every later text-style control
(Textarea, Select shell, FormattedInput, …) inherits without modification.
Where Button proved the variant system on a button surface, Input proves it on a
bordered, focus-ringed, slotted surface — addons and icons live inside the same focus ring,
and invalid / disabled / loading all flow through the same attribute-driven story.
Overview — placeholder, label, error, and icon adornment
Variants
Four "border + background" stories. Pick the one that matches how much frame the surrounding form needs:
outline— 1px border, paper background. The default; safest on any surface.solid— filled tint (bg.subtle), no visible border. Best when the parent is alreadybg.paperand you want to suggest "field" without a heavy frame.ghost— invisible at rest; gains a colored border + tint only on hover / focus. Inline editing and search-as-you-type live here.underline— Material-style single bottom rule. Dense forms.
Every variant works with every color (7 colors) and every theme variant and adapts to the
active platform. The focus ring + focused border use the active color; the body border stays
neutral at rest so colored inputs don't shout for attention.
Anatomy
[ leftAddon | leftIcon | <input /> | rightIcon / spinner | rightAddon ][ leftAddon | leftIcon | <input /> | rightIcon / spinner | rightAddon ]Add-ons are sibling DOM elements that share the wrapper's frame (single border, single radius,
single focus ring). Icons live inside the input padding and are visually decorative — wrap
interactive icons (clear, password reveal) in a real <button> to keep semantics correct.
Examples
Basic
Variants
Sizes
Colors
WithIcons
WithAddons
Invalid
Disabled
Loading
Password
Controlled
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| color | ResponsiveValue<InputColor> | 'primary' | Accent for the focus ring + focused border. |
| fullWidth | ResponsiveValue<boolean> | true | Stretch to fill the parent's inline-size. |
| htmlSize | number | — | Native HTML `size` attribute (character-count hint), since we steal `size` for sizing. |
| invalid | boolean | — | Visual + `aria-invalid` invalid state. Wins over the active `color` for border + ring. |
| leftAddon | ReactNode | — | Sibling element joined visually with the input via a shared border-radius collapse. |
| leftIcon | ReactNode | — | Icon rendered **inside** the input's left padding. Doesn't capture pointer events. |
| loading | boolean | — | When `true`, the field is locked from edits, gains `aria-busy`, and renders a spinner in the right-icon slot. Stays tab-focusable (uses `readOnly`, not `disabled`) so screen readers can still announce the busy state and the field participates in form submission. |
| rightAddon | ReactNode | — | Sibling element joined visually with the input via a shared border-radius collapse. |
| rightIcon | ReactNode | — | Icon rendered **inside** the input's right padding. Replaced by the spinner while `loading`. |
| size | ResponsiveValue<InputSize> | 'md' | Visual height + horizontal padding + font-size. |
| sx | Sx | — | Theme-aware inline style object (resolves palette / spacing / radius tokens to CSS vars). |
| variant | ResponsiveValue<InputVariant> | 'outline' | Border + background story. |
Accessibility
- Renders a native
<input>— keyboard interaction, IME composition, and form submission all use the platform's built-in behaviors. - Labels: Input never auto-labels. Wrap with a
<label htmlFor>(or the future<Field>), passaria-label, or passaria-labelledby. A dev-mode warning fires when none of those are present —[apx-ds][INPUT_NO_LABEL]. - Invalid state:
invalid={true}setsaria-invalid="true"on the input and a matchingdata-invalidon the wrapper. The danger border + ring color come from an attribute selector, so a future<Field invalid>propagates correctly without prop-drilling. - Required state:
requiredsets the native attribute +aria-required="true". aria-describedbyis preserved as-is; the future<Field>will append helper / error text ids here.- Disabled vs read-only:
disabledis unreachable by tab;readOnlyis reachable but uneditable.loadingchooses read-only +aria-busyso the field stays focusable and participates in submission — useful for live validation. - Focus ring lives on the wrapper (
focus-within:ring-*), not the bare input, so addons + icons + input read as a single visual frame.
Theming
Every visual decision is a token. Tweak the global theme to remix the whole library at once, or
scope overrides to Input specifically — both root (wrapper) and input (the bare element) are
addressable slots:
<ThemeProvider
theme={defineTheme({
components: {
Input: {
defaultProps: { variant: 'solid', size: 'lg' },
styleOverrides: {
root: 'shadow-xs',
input: 'font-medium',
leftAddon: 'font-mono',
rightAddon: 'font-mono',
},
},
},
})}
>
{children}
</ThemeProvider><ThemeProvider
theme={defineTheme({
components: {
Input: {
defaultProps: { variant: 'solid', size: 'lg' },
styleOverrides: {
root: 'shadow-xs',
input: 'font-medium',
leftAddon: 'font-mono',
rightAddon: 'font-mono',
},
},
},
})}
>
{children}
</ThemeProvider>Instance-level overrides win via tailwind-merge:
<Input className="border-dashed" sx={{ radius: 'xl' }} style={{ minWidth: 320 }} /><Input className="border-dashed" sx={{ radius: 'xl' }} style={{ minWidth: 320 }} />Do / Don't
- Do pair every Input with a visible
<label>. Placeholders are not labels. - Do use
invalid+ anaria-describedbyerror message for client-side validation. - Don't use
disabledfor "not editable right now" —readOnlykeeps the field reachable and submittable. - Don't put a label-equivalent icon (e.g. an envelope) as the sole accessible name — decorative icons inside the input must be paired with a real label.