Components
Input
The text control: one shell shared with Textarea and SearchInput, so their states can never drift apart.
Examples
One chrome, three controls
Input, Textarea and SearchInput share the same shell, so hover, focus, invalid, disabled and read-only are one implementation and cannot drift. Only the element inside changes.
Bound value: —
<Input placeholder="Instrument or symbol" aria-label="Instrument or symbol" />
<Input placeholder="Read-only value" defaultValue="BTC-USD" readOnly aria-label="Read-only value" />
<Textarea placeholder="Notes for this run" aria-label="Run notes" />
<SearchInput placeholder="Search runs…" value={query} onValueChange={setQuery} />Sizes
One density scale shared by every control: a dense toolbar, a default form, a large onboarding step.
<Input size="sm" placeholder="Small — dense toolbars" aria-label="Small input" />
<Input size="md" placeholder="Medium — default" aria-label="Medium input" />
<Input size="lg" placeholder="Large — forms and onboarding" aria-label="Large input" />States
Hover, focus, invalid, disabled and read-only are shown as specimens, so the treatment is reviewable in a screenshot as well as by using it.
Default
Hover
Border steps up one level
Focus
Accent border and a soft 3px ring
Invalid
`aria-invalid` drives the treatment, so `Field error` sets it for you
Disabled
Out of the tab order; use it only when the value is genuinely unavailable
Read-only
Selectable and copyable, not editable
<Input aria-label="Hover state input" className="border-line-strong" />
<Input aria-label="Focus state input" className="border-line-accent shadow-[var(--glow-focus)]" />
<Input invalid defaultValue="BTC/USD" aria-label="Invalid input" />
<Input disabled aria-label="Disabled input" />
<Input readOnly defaultValue="2026-09-26" aria-label="Read-only input" />A leading icon and a trailing action
Both live inside the one shell: the icon is decorative and brightens with focus, and the trailing slot holds an action, a unit or a clear button.
<Input placeholder="you@luc.dev" leadingIcon={<RiAtLine />} aria-label="Email address" />
<Input
placeholder="Range start"
leadingIcon={<RiCalendarLine />}
trailingAction={<Button size="sm" variant="ghost">Today</Button>}
aria-label="Range start"
/>Field composition
`Field` owns the id, the description and the message, so labelling is never something a page has to remember.
Ticker in BASE/QUOTE form
Select at least one instrument
Bars used to warm up indicators.
<Field label="Symbol" description="Ticker in BASE/QUOTE form" required>
<Input placeholder="BTC/USD" />
</Field>
<Field label="Universe" error="Select at least one instrument">
<Input placeholder="No instruments selected" />
</Field>Usage
import { Input } from "@luc/ui";<Input placeholder="BTC/USD" aria-label="Instrument" />Inside a `Field` the label, description and error message wire themselves to the input; the `aria-label` above is only for a field standing alone.
API
Read from the published TypeScript API, so a prop that is not listed here does not exist and a default that is listed is the one the compiler sees.
| Export | Kind | Signature |
|---|---|---|
controlIconSizeClass | export | Record<ControlSize, string> |
Input | 5 documented props | ({ className, inputClassName, size, leadingIcon, trailingAction, invalid, disabled, readOnly, id, required, ...props }: InputProps) => import("@types/react").JSX.Element |
inputAdornmentClass | export | string |
inputElementClass | export | string |
inputShellClass | export | string |
Input props
Type: InputProps
Values the compiler reads from the declaration.
| Prop | Type | Default | Notes |
|---|---|---|---|
inputClassName? | string | undefined | — | Applied to the `<input>` element itself; `className` styles the shell. |
invalid? | boolean | undefined | — | |
leadingIcon? | ReactNode | — | |
size? | ControlSize | undefined | — | |
trailingAction? | ReactNode | — |
Guidelines
Do
- Wrap the control in `Field`, which owns the id, the described-by relationship and the invalid state.
- Use `size` for the density of the row — a dense toolbar, a default form, an onboarding step — not to make one field louder than its neighbour.
- Keep `leadingIcon` decorative and let the label carry the meaning: the icon slot is `aria-hidden`.
Don't
- Use a placeholder as the label: it disappears the moment the reader types.
- Put two icons in one field with no hierarchy — one leading glyph and one trailing action is the shape the shell is built for.
- Reach for `className` to restyle the shell; `inputClassName` exists for the inner element and the shell classes are the contract.
Accessibility
`Field` supplies the id and the `aria-describedby` relationship, so a message is connected to the control rather than merely near it. `readOnly` keeps the value focusable and copyable and draws the dashed treatment; `disabled` removes the field from the tab order and is the wrong answer for a value a reader may still need to copy.
Related