Input
The canonical text input. Under the official brand it's honest material: an opaque field on a flat surface — no glass, no etched frames (those live in the proposed system). A quiet border at rest; focus turns it gold with a soft gold glow, so you always know exactly where you're typing. Green means the value checks out; red means it needs another look. Disabled picks up a muted fill so it clearly isn't taking input right now. Native input underneath: every HTML type (text, email, password, number, search, …) works without reinventing the wheel.
Input is the atom. For the form unit — label + input + helper text + error text with auto-linked ARIA — compose with /text-field instead of placing <Input> next to loose <label> elements.
States
Five states, five looks — all flat, all friendly. Idle is a quiet border on an opaque field. Hover and focus turn the border gold and add a soft gold glow. Invalid goes red; valid goes green (a positive check passed — the value is good). Disabled takes a muted fill and dimmed text so it clearly isn't asking for input. (The hairline-and-lift treatment specced elsewhere on this page is the proposed system's take.) Severity colors always pair with an icon (ADR-0017); color alone is never the signal.
Password match — live demo of valid + invalid
The canonical use of valid / invalid: a password confirmation. Both fields are valid when they match, invalid when they don't. Edit either to see the states switch live.
Icon slots
Two slots, both optional. Each slot owns its padding so leading and trailing icons line up with the input text. Drop in any ReactNode — a Lucide stroke icon, a Halo SVG, or a short text glyph (currency, units).
Type variants
Input passes through the native HTML type attribute. The visual register is the same; the platform handles keyboard, validation hints, autocomplete, and password masking for free.
Materiality
Materiality is where the two systems part ways most. Glass bodies, etched outlines, and floating lifts all belong to the proposed system. The official input is honest material: an opaque field on a flat surface, one solid border, and a soft gold glow on focus — no transparency, no shadows doing aesthetic work. The spec below documents the proposed treatment; read it as the counterpart, not the official recipe.
- Height
--space-8= 40pxOne canonical height. Pairs with the canonical button height in mixed rows (form + submit) without a vertical mismatch.
- Frame
--space-pxsolid--border-hairlineProposed spec — 1px hairline at rest, transitioning to
--gold-aa-500on hover and focus,--patina-500on valid,--rust-500on invalid, dashed on disabled. The official field keeps one solid border and swaps color instead: gold on focus, green on valid, red on invalid.- Body
- Opaque — official fields carry their own solid surface.
No backdrop-filter here either — but for the opposite reason. The proposed input inherits the glass behind it; the official input is simply a solid, opaque field on a flat page. What you type sits on real material.
- Lift
--shadow-xsat rest,--shadow-smon focusThe floating outset lift is a proposed-system move. Official inputs sit flat — motion lives in buttons and cards (hover lift, press scale), not in fields.
- Corners
--radius-sm= 2pxThe 2px machined-edge look is proposed. Official corners are a touch softer — friendly, not etched.
- Focus ring
--focus-ring(idle) /--focus-ring-error(invalid) /--focus-ring-affirm(valid)These ring tokens are the proposed set. The official focus treatment is a soft 3px gold glow; invalid swaps it to red, valid to green. Same job, warmer delivery.
Props
- invalid
booleanRenders the red error border and sets
aria-invalidon the underlying input. Pair with an error icon (trailing slot) or an error message (via TextField) — color alone is never the signal (ADR-0017).- valid
booleanThe positive sibling of
invalid. Renders the green success border and setsaria-invalid="false"(the explicit "validated, no problem" signal). Use after a positive check — password match, email format OK, username available. Mutually exclusive withinvalid; if both are set, invalid wins (errors are always loudest).- leadingIcon
ReactNodeContent before the value. Lucide stroke icon, Halo SVG, or a short text glyph (currency, unit).
- trailingIcon
ReactNodeContent after the value. Validation tick, clear button, unit suffix. Use sparingly.
- …native attrs
InputHTMLAttributesAll standard input attributes pass through:
type,value,onChange,placeholder,required,autoComplete,disabled, etc.sizeis omitted from the prop surface (no size variant; it's a native input attr unrelated to visual sizing).- ref
Ref<HTMLInputElement>Forwarded to the underlying input, not the wrapper. Use for imperative focus, value reads, etc.
Usage
import { Input } from '@halo-compliance/ui'
<Input
placeholder="Search docs"
value={query}
onChange={(e) => setQuery(e.target.value)}
/><Input
type="email"
placeholder="you@lender.com"
leadingIcon={<MailIcon />}
trailingIcon={isValid ? <CheckIcon /> : null}
value={email}
onChange={(e) => setEmail(e.target.value)}
/><TextField>
<TextField.Label>Email</TextField.Label>
<TextField.Input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
invalid={!!error}
/>
{error && <TextField.ErrorText>{error}</TextField.ErrorText>}
</TextField>When to reach for which
- Input — when you need just the field. A controlled search box in chrome, a value-only cell in a table, a one-off prompt.
- TextField — for any form field with a label or helper text. The compound pattern wires ARIA correctly so the label, helper, and error text all describe the right input.
- CommandPaletteTrigger — when the affordance looks like a field but the action is "open a palette." The trigger borrows the field aesthetic deliberately, but it's a button (no value).
Anti-patterns
- Don't drop a bare
<input>into routes. The eslint ruleno-bare-input-elementcatches it. Bare inputs drift on border, focus glow, and disabled styling. - Don't put a label inside the input as placeholder. Placeholder is for the example value, not the label. Use a real
<label>(or TextField) so screen readers and the WCAG label-association rules are satisfied. - Don't carry severity by color alone. Invalid pairs with an icon in the trailing slot or a message via TextField (ADR-0017). Same rule that governs Card variants and Button variants.
- Don't restyle the frame at the consumer. Inputs ship with one canonical look: quiet border, gold focus, green valid, red invalid. If a tier feels missing, that's a substrate conversation, not a one-off override.
- Don't show valid before the user has done anything. Valid is the result of a positive check (match, format-OK, available) — not the default. Idle is the right starting state; switch to valid only when validation has actually passed.
Related
- /text-field — the form pattern that wraps Input with label + helper + error and auto-links ARIA.
- /button — pairs with Input in submit rows; the form-input height matches the canonical button height.
- /app-bar#middle — where the search-shaped affordance lives in chrome. CommandPaletteTrigger borrows Input's visual register.
- /tokens —
--shadow-xs,--shadow-sm,--focus-ring,--focus-ring-error,--focus-ring-affirmland here.