Skip to main content
Markdown

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


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

Input

See Spar Input docs for primitive behavior.

Props

NameTypeDefaultDescription
childrenReact.ReactNode-Compound parts (Input.Field, optional affixes, icons, clear/spinner/reveal/stepper actions). Wrap in a Field to attach labels and helper text.
sizeInputSize'base'Size scale.
classNamesPartial<Record<"root", string>>-Per-slot class name overrides.
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-Per-slot HTML attribute overrides.
idstring-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.
disabledboolean-Input disabled state. When inside a Field, inherited from Field.
requiredboolean-Input required state. When inside a Field, inherited from Field.
readOnlyboolean-Input read-only state. When inside a Field, inherited from Field.
invalidboolean-Input validation state. When inside a Field, inherited from Field.
classNamestring-Appends custom classes to the root slot of this part.

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-sizeAlwaysReflects 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-invalidWhen invalid is true.Theme hook for the invalid state. Emitted by Spar Input on the root.
data-disabledWhen disabled is true.Theme hook for the disabled state. Emitted by Spar Input.
data-requiredWhen required is true.Theme hook used by the parent Field to auto-render its required asterisk.
data-readonlyWhen readOnly is true.Theme hook for the read-only state. Emitted by Spar Input.

Input.Field

See Spar Input docs for primitive behavior.

Props

NameTypeDefaultDescription
classNamesPartial<Record<"root", string>>-Per-slot class name overrides.
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-Per-slot HTML attribute overrides.
autoFocusbooleanfalseWhether to focus the input on mount
classNamestring-Appends custom classes to the root slot of this part.

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Prefix

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Suffix

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.LeadingIcon

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.TrailingIcon

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.ClearButton

Events

NameTypeDefaultDescription
onClear() => void-Called after the field value is cleared.

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Spinner

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.RevealButton

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Strength

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-levelOn each filled segment.Strength tier of the current field value: weak, medium, or strong.

Input.Stepper

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Decrement

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Increment

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Input.Chips

Props

NameTypeDefaultDescription
childrenReact.ReactNode-Optional extra content rendered after the auto-generated chip tokens.
valuestring[]-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.
defaultValuestring[]-Initial tags for uncontrolled usage.
separatorstring-Optional character that commits the field text as a tag (Enter always commits).
maxnumber-Maximum number of tags. Further commits are ignored once reached.
allowDuplicatesbooleanfalseAllow committing a tag that already exists.
classNamesPartial<Record<"root", string>>-Per-slot class name overrides.
slotPropsPartial<Record<"root", React.HTMLAttributes<HTMLElement>>>-Per-slot HTML attribute overrides.
classNamestring-Appends custom classes to the root slot of this part.

Events

NameTypeDefaultDescription
onValueChange(value: string[]) => void-Called with the next tag array after a commit or removal.

Data attributes

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.

Type Definitions

NameDefinition
InputSize'small' | 'base' | 'large'