Input is a compound, accessible text input. The root renders Spar Input,
owns the bordered row, and hosts the field plus optional affixes, icons, and
actions. Wrap Input in a Field to attach a label, helper text, or error
message — the field-level state cascades into the input automatically.
Usage
import { Field, Input } from '@takeoff-ui/react-spar';
<Field>
<Field.Label />
<Input>
<Input.LeadingIcon />
<Input.Prefix />
<Input.Chips />
<Input.Field />
<Input.Suffix />
<Input.TrailingIcon />
<Input.ClearButton />
<Input.Spinner />
<Input.RevealButton />
<Input.Stepper>
<Input.Decrement />
<Input.Increment />
</Input.Stepper>
<Input.Strength />
</Input>
<Field.Description />
<Field.ErrorMessage />
</Field>
Compose the parts à la carte — an input only needs the ones its variant calls
for (password → Input.RevealButton / Input.Strength, number → the stepper,
tags → Input.Chips). Input.Strength is authored inside Input (it reads the
field value from context) but renders just below the bordered row, and
Input.Chips renders each committed tag as a removable Chip.
Playground
function PlaygroundDemo() {
return (
<Field className="w-full max-w-90">
<Field.Label>Passenger name</Field.Label>
<Input>
<Input.Field placeholder="Ada Lovelace" />
</Input>
<Field.Description>
Match the name on your travel document.
</Field.Description>
</Field>
);
}
render(<PlaygroundDemo />);
Using with Field
Field is the generic ARIA wrapper. Setting invalid, disabled, required,
optional, or readOnly on Field cascades into the nested Input, and
Field.Label, Field.Description, and Field.ErrorMessage are wired to the
field control through shared IDs.
function FieldWithInputDemo() {
return (
<div className="grid w-full max-w-90 gap-3">
<Field required>
<Field.Label>Email</Field.Label>
<Input>
<Input.Field type="email" placeholder="you@example.com" />
</Input>
<Field.Description>We will send a booking confirmation.</Field.Description>
</Field>
<Field invalid>
<Field.Label>Phone number</Field.Label>
<Input>
<Input.Field defaultValue="not-a-phone" />
</Input>
<Field.ErrorMessage>Enter a valid phone number.</Field.ErrorMessage>
</Field>
<Field optional>
<Field.Label>Frequent flyer number</Field.Label>
<Input>
<Input.Field placeholder="Optional" />
</Input>
</Field>
<Field disabled>
<Field.Label>Disabled</Field.Label>
<Input>
<Input.Field defaultValue="Booking locked" />
</Input>
</Field>
<Field readOnly>
<Field.Label>Read-only</Field.Label>
<Input>
<Input.Field defaultValue="TK-1928" />
</Input>
</Field>
</div>
);
}
render(<FieldWithInputDemo />);
Sizes
function SizesDemo() {
return (
<div className="grid w-full max-w-90 gap-3">
<Field>
<Field.Label>Small</Field.Label>
<Input size="small">
<Input.Field placeholder="Compact" />
</Input>
</Field>
<Field>
<Field.Label>Base</Field.Label>
<Input size="base">
<Input.Field placeholder="Default" />
</Input>
</Field>
<Field>
<Field.Label>Large</Field.Label>
<Input size="large">
<Input.Field placeholder="Roomy" />
</Input>
</Field>
</div>
);
}
render(<SizesDemo />);
Prefix, Suffix & Icons
function AdornmentsDemo() {
return (
<div className="grid w-full max-w-90 gap-3">
<Field>
<Field.Label>Amount</Field.Label>
<Input>
<Input.Prefix>USD</Input.Prefix>
<Input.Field placeholder="0.00" inputMode="decimal" />
<Input.Suffix>.00</Input.Suffix>
</Input>
</Field>
<Field>
<Field.Label>Search flights</Field.Label>
<Input>
<Input.LeadingIcon>
<SearchIconOutlinedRounded width={16} height={16} />
</Input.LeadingIcon>
<Input.Field placeholder="IST to LHR" />
<Input.TrailingIcon>
< TakeoffRocketIconOutlinedRounded width={16} height={16} />
</Input.TrailingIcon>
</Input>
</Field>
</div>
);
}
render(<AdornmentsDemo />);
Actions
function ActionsDemo() {
const [loading, setLoading] = React.useState(false);
return (
<div className="flex flex-col gap-4 w-full max-w-90">
<Field>
<Field.Label>Search booking</Field.Label>
<Input>
<Input.Field defaultValue="TK1928" />
<Input.ClearButton />
</Input>
</Field>
<Field>
<Field.Label>PNR lookup</Field.Label>
<Input>
<Input.Field placeholder="ABC123" onFocus={() => setLoading(true)} onBlur={() => setLoading(false)} />
{loading && <Input.Spinner />}
</Input>
</Field>
<Field>
<Field.Label>Password</Field.Label>
<Input>
<Input.Field type="password" placeholder="Enter password" />
<Input.RevealButton />
</Input>
</Field>
</div>
);
}
render(<ActionsDemo />);
Password
Compose a password field from the design-system parts: a leading lock icon,
Input.RevealButton to toggle visibility, and Input.Strength for the
four-segment strength meter. The meter grades the live field value (length plus
upper/lower case, digits, and symbols) and recolours from weak to strong.
function PasswordDemo() {
return (
<div className="w-full max-w-90">
<Field required>
<Field.Label>Password</Field.Label>
<Input>
<Input.LeadingIcon>
<LockOpenIconOutlinedRounded/>
</Input.LeadingIcon>
<Input.Field type="password" placeholder="Enter password" />
<Input.RevealButton />
<Input.Strength />
</Input>
<Field.Description>
Use 8+ characters with a mix of letters, numbers & symbols.
</Field.Description>
</Field>
</div>
);
}
render(<PasswordDemo />);
Number
Input.Field type="number" passes the native numeric attributes — min, max,
step, and inputMode — straight through to the control. Compose
Input.Stepper when the design needs explicit increment and decrement buttons;
the buttons use the native input stepping API.
function NumberDemo() {
return (
<div className="w-full max-w-90">
<Field>
<Field.Label>Checked bags</Field.Label>
<Input>
<Input.Field type="number" defaultValue={1} inputMode="numeric" />
<Input.Stepper>
<Input.Decrement />
<Input.Increment />
</Input.Stepper>
</Input>
<Field.Description>Type a value or use the steppers.</Field.Description>
</Field>
</div>
);
}
render(<NumberDemo />);
Number stepping is delegated to the native input: Input.Decrement /
Input.Increment call stepDown() / stepUp(). Add min, max, and step
to Input.Field to bound the value — at a limit the browser keeps the value
instead of stepping past it (the platform validates typed values rather than
clamping on blur).
Counter
The counter layout centers the value between brand-coloured buttons. Select it
with data-layout="counter" on the Input root and place Input.Decrement /
Input.Increment flanking Input.Field. The recipe keys on the explicit
attribute, so the look no longer depends on where the buttons sit in the DOM.
function CounterDemo() {
return (
<div className="w-full max-w-90">
<Field>
<Field.Label>Passengers</Field.Label>
<Input data-layout="counter">
<Input.Decrement aria-label="Remove passenger">−</Input.Decrement>
<Input.Field type="number" defaultValue={2} inputMode="numeric" />
<Input.Increment aria-label="Add passenger">+</Input.Increment>
</Input>
<Field.Description>Steppers flank a centered value.</Field.Description>
</Field>
</div>
);
}
render(<CounterDemo />);
Migration: earlier the counter look was selected purely by DOM placement —
Input.Decrement / Input.Increment as direct children flanking the field.
That detection is gone; add data-layout="counter" to the Input root to opt
in. Without it the same markup now renders as a plain left-aligned field.
Chips
Input.Chips turns the input into a tag field. It owns the string[] value
(controlled via value / onValueChange, or uncontrolled via defaultValue)
and renders each tag as a removable Chip (in the neutral / outlined
parity look). Place it next to Input.Field: Enter (or the optional separator
character) commits the trimmed field text and Backspace on an empty field
removes the last tag. max caps the tag count — commits past the cap are
ignored — and allowDuplicates permits repeats.
function ChipsDemo() {
const [tags, setTags] = React.useState(['Istanbul', 'London']);
return (
<div className="w-full max-w-90">
<Field>
<Field.Label>Destinations</Field.Label>
<Input>
<Input.Chips value={tags} onValueChange={setTags} separator="," />
<Input.Field placeholder="Type a city and press Enter" />
</Input>
<Field.Description>
Press Enter or comma to add a destination. Backspace removes the last one.
</Field.Description>
</Field>
</div>
);
}
render(<ChipsDemo />);
Object-valued tags and per-chip options are out of scope for now; the chips
value is a string[]. For input masking / formatting (dates, phone numbers,
currency), wire a formatter into Input.Field's onChange — the compound stays
format-agnostic, matching Spar's headless input.
Textarea
function TextareaDemo() {
return (
<div className="w-full max-w-90">
<Field>
<Field.Label>Special assistance note</Field.Label>
<Input>
<Input.Field as="textarea" rows={4} placeholder="Add any details for the airport team." />
</Input>
<Field.Description>
This note is shared with the ground operations team.
</Field.Description>
</Field>
</div>
);
}
render(<TextareaDemo />);
Customizing slots
Every compound part forwards slotProps (native attributes, style, aria-*)
to its slot owner node, and classNames for CSS classes — so you can shape a
part without re-implementing it. This search template rounds the root into a
pill, tints the leading icon with the brand colour, and turns off the field's
autocomplete, all through slotProps.
function SearchTemplate() {
return (
<div className="w-full max-w-90">
<Field>
<Field.Label>Search flights</Field.Label>
<Input slotProps={{ root: { style: { borderRadius: '9999px' } } }}>
<Input.LeadingIcon slotProps={{ root: { style: { color: 'var(--primary-base)' } } }}>
<SearchIconOutlinedRounded width={16} height={16} />
</Input.LeadingIcon>
<Input.Field
placeholder="Where to?"
slotProps={{ root: { autoComplete: 'off', style: { fontWeight: 500 } } }}
/>
</Input>
</Field>
</div>
);
}
render(<SearchTemplate />);
Accessibility
Field.Label, Field.Description, and Field.ErrorMessage are wired to the
control via stable IDs derived from the Field root (aria-labelledby,
aria-describedby, aria-invalid).
- The asterisk inside
Field.Label is decorative — required is also surfaced
to assistive tech via the input's native required / aria-required.
Input.LeadingIcon and Input.TrailingIcon default to aria-hidden="true".
Use Input.ClearButton or Input.RevealButton for focusable actions.
Input.Decrement and Input.Increment default to icon-only button labels of
"Decrement value" and "Increment value". Native type="number" keeps the
spinbutton semantics.
API Reference
See Spar Input docs
for primitive behavior.
| Name | Type | Default | Description |
|---|
| children | React.ReactNode | - | Compound parts (Input.Field, optional affixes, icons, clear/spinner/reveal/stepper actions). Wrap in a Field to attach labels and helper text. |
| size | InputSize | 'base' | Size scale. |
| classNames | Partial<Record<"root", string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<"root", React.HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| id | string | - | Custom base ID for ARIA relationships. If not provided, one will be generated automatically. Sub-element IDs are derived as ${id}-field, ${id}-label, etc. |
| disabled | boolean | - | Input disabled state. When inside a Field, inherited from Field. |
| required | boolean | - | Input required state. When inside a Field, inherited from Field. |
| readOnly | boolean | - | Input read-only state. When inside a Field, inherited from Field. |
| invalid | boolean | - | Input validation state. When inside a Field, inherited from Field. |
| className | string | - | Appends custom classes to the root slot of this part. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-size | Always | Reflects the resolved size prop so theme recipes can scope size variants. |
| data-layout="counter" | When set by the consumer. | Opts into the counter look (centered value, brand-coloured flanking Input.Decrement / Input.Increment). Replaces the former DOM-placement detection. |
| data-invalid | When invalid is true. | Theme hook for the invalid state. Emitted by Spar Input on the root. |
| data-disabled | When disabled is true. | Theme hook for the disabled state. Emitted by Spar Input. |
| data-required | When required is true. | Theme hook used by the parent Field to auto-render its required asterisk. |
| data-readonly | When readOnly is true. | Theme hook for the read-only state. Emitted by Spar Input. |
See Spar Input docs
for primitive behavior.
| Name | Type | Default | Description |
|---|
| classNames | Partial<Record<"root", string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<"root", React.HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| autoFocus | boolean | false | Whether to focus the input on mount |
| className | string | - | Appends custom classes to the root slot of this part. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Name | Type | Default | Description |
|---|
| onClear | () => void | - | Called after the field value is cleared. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-level | On each filled segment. | Strength tier of the current field value: weak, medium, or strong. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Name | Type | Default | Description |
|---|
| children | React.ReactNode | - | Optional extra content rendered after the auto-generated chip tokens. |
| value | string[] | - | Committed tags (controlled). Pair with onValueChange. Spar's Input is a scalar primitive with no array model, so the chips value is owned here as a react-enhancement rather than picked from Spar. |
| defaultValue | string[] | - | Initial tags for uncontrolled usage. |
| separator | string | - | Optional character that commits the field text as a tag (Enter always commits). |
| max | number | - | Maximum number of tags. Further commits are ignored once reached. |
| allowDuplicates | boolean | false | Allow committing a tag that already exists. |
| classNames | Partial<Record<"root", string>> | - | Per-slot class name overrides. |
| slotProps | Partial<Record<"root", React.HTMLAttributes<HTMLElement>>> | - | Per-slot HTML attribute overrides. |
| className | string | - | Appends custom classes to the root slot of this part. |
| Name | Type | Default | Description |
|---|
| onValueChange | (value: string[]) => void | - | Called with the next tag array after a commit or removal. |
| Attribute | Applied when | Purpose |
|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| Name | Definition |
|---|
| InputSize | 'small' | 'base' | 'large' |