Skip to main content

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.

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.


function MaskShapeDemo() {
  const [meta, setMeta] = React.useState({ raw: '', completed: false });
  return (
    <div className="w-full max-w-90">
      <Field>
        <Field.Label>Card number</Field.Label>
        <Input>
          <Input.Field
            inputMode="numeric"
            placeholder="0000 0000 0000 0000"
            mask={{ blocks: [4, 4, 4, 4], delimiter: ' ', numericOnly: true }}
            onValueChange={(value, next) => setMeta(next)}
          />
          <Input.ClearButton />
        </Input>
        <Field.Description>
          raw: {meta.raw || '—'} · completed: {String(meta.completed)}
        </Field.Description>
      </Field>
    </div>
  );
}

render(<MaskShapeDemo />);

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.


function MaskRegexDemo() {
  const [completed, setCompleted] = React.useState(false);
  return (
    <div className="w-full max-w-90">
      <Field>
        <Field.Label>Flight number</Field.Label>
        <Input>
          <Input.Field
            placeholder="TK1985"
            mask={{ regex: /^[A-Z]{2}\d{1,4}$/, uppercase: true }}
            onValueChange={(value, meta) => setCompleted(meta.completed)}
          />
        </Input>
        <Field.Description>
          Two letters, then up to four digits. completed: {String(completed)}
        </Field.Description>
      </Field>
    </div>
  );
}

render(<MaskRegexDemo />);

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]).


function MaskPresetDemo() {
  const [departure, setDeparture] = React.useState({});
  const [boarding, setBoarding] = React.useState({});
  return (
    <div className="flex w-full max-w-180 flex-col gap-4 sm:flex-row">
      <Field className="flex-1">
        <Field.Label>Departure date</Field.Label>
        <Input>
          <Input.Field
            inputMode="numeric"
            placeholder="dd/mm/yyyy"
            mask={{ date: true, delimiter: '/', dateMin: '2026-01-01', dateMax: '2030-12-31' }}
            onValueChange={(value, meta) => setDeparture(meta)}
          />
        </Input>
        <Field.Description>iso: {departure.iso ?? '—'}</Field.Description>
      </Field>
      <Field className="flex-1">
        <Field.Label>Boarding time</Field.Label>
        <Input>
          <Input.Field
            inputMode="numeric"
            placeholder="hh:mm"
            mask={{ time: true, delimiter: ':' }}
            onValueChange={(value, meta) => setBoarding(meta)}
          />
        </Input>
        <Field.Description>iso: {boarding.iso ?? '—'}</Field.Description>
      </Field>
    </div>
  );
}

render(<MaskPresetDemo />);

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.


function MaskNumberDemo() {
  const [fare, setFare] = React.useState({});
  return (
    <div className="flex w-full max-w-180 flex-col gap-4 sm:flex-row">
      <Field className="flex-1">
        <Field.Label>Fare</Field.Label>
        <Input>
          <Input.Prefix>TRY</Input.Prefix>
          <Input.Field
            inputMode="decimal"
            placeholder="0,00"
            mask={{ number: true, numberLocale: 'tr-TR' }}
            onValueChange={(value, meta) => setFare(meta)}
          />
        </Input>
        <Field.Description>
          iso: {fare.iso ?? '—'} · two passengers:{' '}
          {fare.iso ? (Number(fare.iso) * 2).toLocaleString('tr-TR') : '—'}
        </Field.Description>
      </Field>

      <Field className="flex-1">
        <Field.Label>Baggage allowance</Field.Label>
        <Input>
          <Input.Field
            inputMode="numeric"
            placeholder="0"
            mask={{
              number: true,
              numberLocale: 'tr-TR',
              numberDecimalScale: 0,
              numberIntegerScale: 3,
              numberPositiveOnly: true,
            }}
          />
          <Input.Suffix>kg</Input.Suffix>
        </Input>
        <Field.Description>Whole kilos, three digits, never negative.</Field.Description>
      </Field>
    </div>
  );
}

render(<MaskNumberDemo />);

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:

FieldOmitted means
rawvalue 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.
completedtrue. A mask with no notion of being unfinished must not be able to block submit logic.
isoabsent. 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.

function MaskResolverDemo() {
  const [meta, setMeta] = React.useState({});

  const ibanMask = React.useCallback((raw) => {
    // A Turkish IBAN is 'TR' and then 24 digits. Accepting per position rather
    // than per character class is what pins the country code to the front: a
    // character that does not belong where it landed is dropped, so nothing
    // after it shifts.
    const clean = [...raw.toUpperCase()].reduce((accepted, char) => {
      if (accepted.length >= 26) return accepted;
      const wanted = 'TR'[accepted.length];
      if (wanted) return char === wanted ? accepted + char : accepted;
      return char >= '0' && char <= '9' ? accepted + char : accepted;
    }, '');

    return {
      value: clean.replace(/(.{4})(?=.)/g, '$1 '),
      iso: clean,
      completed: clean.length === 26,
    };
  }, []);

  return (
    <div className="w-full max-w-120">
      <Field>
        <Field.Label>IBAN</Field.Label>
        <Input>
          <Input.Field
            placeholder="TR33 0006 1005 1978 6457 8413 26"
            mask={ibanMask}
            onValueChange={(value, next) => setMeta(next)}
          />
        </Input>
        <Field.Description>
          iso: {meta.iso || '—'} · {(meta.raw ?? '').length}/26
        </Field.Description>
      </Field>
    </div>
  );
}

render(<MaskResolverDemo />);

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.


function MaskComposeDemo() {
  const [meta, setMeta] = React.useState({});

  // createDateMask returns a plain resolver, so it can be wrapped like any
  // other function: run the built-in first, then add the one rule it cannot
  // express — dateMin/dateMax bound a range, not a weekday.
  const businessDayMask = React.useMemo(() => {
    const base = createDateMask({ date: true, delimiter: '.' });
    return (raw, ctx) => {
      const result = base(raw, ctx);
      if (!result.completed || !result.iso) return result;
      const weekday = new Date(result.iso + 'T00:00:00Z').getUTCDay();
      const isWeekend = weekday === 0 || weekday === 6;
      return isWeekend ? { ...result, completed: false, iso: undefined } : result;
    };
  }, []);

  return (
    <div className="w-full max-w-90">
      <Field>
        <Field.Label>Settlement date</Field.Label>
        <Input>
          <Input.Field
            inputMode="numeric"
            placeholder="dd.mm.yyyy"
            mask={businessDayMask}
            onValueChange={(value, next) => setMeta(next)}
          />
        </Input>
        <Field.Description>
          {meta.iso
            ? 'Business day — ' + meta.iso
            : meta.completed === false && (meta.raw ?? '').length === 8
              ? 'That is a weekend; pick a business day.'
              : 'Weekends are rejected even once the date is well-formed.'}
        </Field.Description>
      </Field>
    </div>
  );
}

render(<MaskComposeDemo />);

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.


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[]. Masking and formatting are no longer something you wire into onChange by hand — see Mask for the built-in mask prop.

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.
maskMask-Input mask — a shape/date/time/number/regex pattern, or a resolver function.
autoFocusbooleanfalseWhether to focus the input on mount
classNamestring-Appends custom classes to the root slot of this part.

Events​

NameTypeDefaultDescription
onValueChange(value: string, meta: MaskChangeMeta) => void-Fires with the masked value plus raw / completed / iso metadata.

Data attributes​

AttributeApplied whenPurpose
data-slot="root"AlwaysStable selector for wrapper styling on the root slot.
data-maskmask is setEmitted by Spar. Presence only — never the pattern itself. The sanctioned hook for styling a masked field without reading its mask prop.
data-mask-completedmask is set and the value fills itEmitted 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​

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 and the typed text stays in the field.
allowDuplicatesbooleanfalseAllow committing a tag that already exists. A rejected duplicate stays in the field as typed text.
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'
MaskMaskPattern | MaskPreset | MaskResolver
MaskChangeMeta{ raw: string; completed: boolean; iso?: string }