Input
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
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.
Sizes
Prefix, Suffix & Icons
Actions
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.
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.
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.
Migration: earlier the counter look was selected purely by DOM placement —
Input.Decrement/Input.Incrementas direct children flanking the field. That detection is gone; adddata-layout="counter"to theInputroot to opt in. Without it the same markup now renders as a plain left-aligned field.
Mask
Input.Field takes a mask prop and, while it is set, reports every edit
through onValueChange(value, meta). meta carries raw (the value with
separators stripped), completed, and — when the mask defines a canonical form
— iso. Prefer onValueChange over onChange on a masked field: deletions and
undo are applied directly to the control, so they never surface as a React
change event.
Caret handling, delete/undo behaviour and paste are all handled by Spar. There are only two engine patterns, and both are pure mechanics:
Shape
blocks lays characters into groups and puts a delimiter between them.
numericOnly / letterOnly filter what is accepted, uppercase / lowercase
case the result.
Input.ClearButton above is not decoration — it proves the wiring. Backspacing
to empty is applied imperatively by the mask, and the button still disappears,
because Input.Field mirrors onValueChange back into the Input context that
Input.ClearButton and Input.Strength read.
Regex
regex is matched one character at a time: write the pattern for the final
value and partial states are derived from it. The pattern is the whole
specification, so blocks and delimiters do not apply — a character is either
accepted or dropped.
Date & time
{ date: true } and { time: true } do something the two engine patterns
cannot: they clamp the typed value into a legal range. Typing 4 into the day
block yields 04, 35 becomes 31, 13 in the month block becomes 12, and
dateMin / dateMax bound the year. Once the value is complete, meta.iso
carries YYYY-MM-DD (or HH:mm[:ss]).
Number
{ number: true } groups the integer part as it grows and keeps one decimal
mark. It is the only mask with no fixed length, so it has no blocks — grouping
is variable-width and applied right to left.
Separators and group sizes come from Intl.NumberFormat, so numberLocale is
the whole configuration: 'tr-TR' gives 1.234.567,89, 'de-DE' the same,
'en-US' gives 1,234,567.89, and 'en-IN' gives lakh grouping
(1,12,34,567) without a separate option. Override just the character with
delimiter (delimiter: '' turns grouping off) or just the mark with
numberDecimalMark. numberDecimalScale caps the fraction (0 makes the field
integer-only and the mark inert), numberIntegerScale caps the integer digits,
and numberPositiveOnly rejects the minus sign.
meta.raw keeps the sign and the decimal mark — those are part of the number,
not punctuation between blocks — while meta.iso is always a
Number()-parseable string with a . fraction, so it is what you send to the
server.
Resolvers
Anything else is a function. It receives the candidate string plus a context
(caret, previousValue, inputType) and returns { value }. Caret placement
stays inside Spar, so a resolver only decides what the value is.
The rest of meta is derived from value unless the resolver overrides it —
omitting a field is a default, not an absence:
| Field | Omitted means |
|---|---|
raw | value with every separator stripped. A resolver that formats TR330006… as TR33 0006 … still reports the unspaced 26 characters, so a length check counts content, not punctuation. |
completed | true. A mask with no notion of being unfinished must not be able to block submit logic. |
iso | absent. Return one where the mask has a canonical machine value, the way date reports YYYY-MM-DD. |
insignificant | /[^\p{L}\p{N}]/u — what raw strips and what the caret skips over. Override it when "separator" means something else for this mask. |
This is the full contract, not a fallback. date, time and number are
themselves resolvers — { date: true } is exactly
createDateMask({ date: true }) — so a built-in has no capability your own mask
lacks, and can be wrapped like any other function when you need one rule it does
not express.
All three factories — createDateMask, createTimeMask, createNumberMask —
and the Mask* types are re-exported from @takeoff-ui/react-spar so a typed
resolver has the same contract Spar validates against.
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. An ignored commit (cap reached,
or a duplicate while allowDuplicates is off) leaves the typed text in the
field. Removing a tag from its remove button moves focus to the neighbouring
tag's remove button, or back to the field when it was the last one.
Object-valued tags and per-chip options are out of scope for now; the chips
value is a string[]. Masking and formatting are no longer something you wire
into onChange by hand — see Mask for the built-in mask prop.
Textarea
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.
Accessibility
Field.Label,Field.Description, andField.ErrorMessageare wired to the control via stable IDs derived from theFieldroot (aria-labelledby,aria-describedby,aria-invalid).- The asterisk inside
Field.Labelis decorative —requiredis also surfaced to assistive tech via the input's nativerequired/aria-required. Input.LeadingIconandInput.TrailingIcondefault toaria-hidden="true". UseInput.ClearButtonorInput.RevealButtonfor focusable actions.Input.DecrementandInput.Incrementdefault to icon-only button labels of "Decrement value" and "Increment value". Nativetype="number"keeps the spinbutton semantics.
API Reference
Input
See Spar Input docs for primitive behavior.
Props
| 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. |
Data attributes
| 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. |
Input.Field
See Spar Input docs for primitive behavior.
Props
| 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. |
| mask | Mask | - | Input mask — a shape/date/time/number/regex pattern, or a resolver function. |
| autoFocus | boolean | false | Whether to focus the input on mount |
| className | string | - | Appends custom classes to the root slot of this part. |
Events
| Name | Type | Default | Description |
|---|---|---|---|
| onValueChange | (value: string, meta: MaskChangeMeta) => void | - | Fires with the masked value plus raw / completed / iso metadata. |
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
| data-mask | mask is set | Emitted by Spar. Presence only — never the pattern itself. The sanctioned hook for styling a masked field without reading its mask prop. |
| data-mask-completed | mask is set and the value fills it | Emitted by Spar. Mirrors meta.completed from onValueChange, so a field can be styled as finished without lifting that state into React. |
Input.Prefix
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Suffix
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.LeadingIcon
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.TrailingIcon
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.ClearButton
Events
| Name | Type | Default | Description |
|---|---|---|---|
| onClear | () => void | - | Called after the field value is cleared. |
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Spinner
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.RevealButton
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Strength
Data attributes
| 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. |
Input.Stepper
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Decrement
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Increment
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Input.Chips
Props
| 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 and the typed text stays in the field. |
| allowDuplicates | boolean | false | Allow committing a tag that already exists. A rejected duplicate stays in the field as typed text. |
| 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. |
Events
| Name | Type | Default | Description |
|---|---|---|---|
| onValueChange | (value: string[]) => void | - | Called with the next tag array after a commit or removal. |
Data attributes
| Attribute | Applied when | Purpose |
|---|---|---|
| data-slot="root" | Always | Stable selector for wrapper styling on the root slot. |
Type Definitions
| Name | Definition |
|---|---|
| InputSize | 'small' | 'base' | 'large' |
| Mask | MaskPattern | MaskPreset | MaskResolver |
| MaskChangeMeta | { raw: string; completed: boolean; iso?: string } |