Skip to main content

Input

betav0.1.0

Base single-line text field for forms. Standalone component built on a native <input> with integrated label, required marker, info tooltip trigger, helper/error text, optional character-limit counter (tied to maxLength), an optional leading startIcon (decorative), and an optional trailing endIcon slot (decorative icon or interactive IconButton — password toggle, copy, attach, clear). Covers MUI TextField and @filigran/ui Input. Split from the former "Inputs" umbrella into its own dedicated entry — does not cover Select, FormControl or InputLabel.

Variants

We'll never share your email
20/120
This name is already taken
1 to 65535 · ArrowUp / ArrowDown step too
The increment arrow disables itself at the bound
Not one of the permitted scores

Examples

<Input label="Name" placeholder="Jane Doe" onChange={(e) => console.log(e.target.value)} />
<Input label="Email" type="email" required helperText="We'll never share your email" />
<Input
  label="API key"
  infoTooltip={
    <TooltipProvider>
      <Tooltip>
        <TooltipTrigger asChild>
          <IconButton
            icon={<Icon name="info" size={16} className="text-feedback-info-primary" />}
            aria-label="What is this?"
            variant="default"
            priority="tertiary"
            size="sm"
          />
        </TooltipTrigger>
        <TooltipContent>Found in Settings → API access</TooltipContent>
      </Tooltip>
    </TooltipProvider>
  }
/>
<Input
  label="Password"
  type={showPassword ? "text" : "password"}
  endIcon={{
    type: "iconButton",
    icon: <Icon name={showPassword ? "eye-off" : "eye"} size={16} />,
    label: showPassword ? "Hide password" : "Show password",
    onClick: () => setShowPassword((v) => !v),
  }}
/>
<Input
  label="Invite link"
  defaultValue="https://filigran.io/i/ab12cd"
  endIcon={{
    type: "iconButton",
    icon: <Icon name="clipboard-list" size={16} />,
    label: "Copy to clipboard",
    onClick: () => navigator.clipboard.writeText("https://filigran.io/i/ab12cd"),
  }}
/>
<Input label="Bio" maxLength={120} defaultValue="Threat intel analyst" helperText="Shown on your public profile" />
<Input label="Username" startIcon={<Icon name="user" size={16} />} placeholder="username" />
<Input label="Display name" error="This name is already taken" defaultValue="admin" />
<Input label="Workspace slug" success defaultValue="filigran-prod" />
<Input label="Port" isTypeNumber defaultValue="8080" min={1} max={65535} helperText="1 to 65535" />
// step drives both the arrows and ArrowUp/ArrowDown; the arrows disable at the bounds
<Input label="Confidence" isTypeNumber defaultValue="50" step={10} min={0} max={100} />
// a unit is read after the value (aria-describedby), never decorative
<Input label="Regex group rules" endText="/gm" defaultValue="^(?<user>\w+)@" helperText="Flags are applied to the whole pattern" />
<Input label="Organization" disabled defaultValue="Filigran" />
<Input aria-label="Search unlabeled" placeholder="No visible label — aria-label required" />

Props

PropType
type"text" | "password" | "number" | "email" — Input type (default: "text")
valuestring — Controlled value
defaultValuestring — Default value (uncontrolled)
onChange(event: ChangeEvent<HTMLInputElement>) => void — Fires on each keystroke
labelstring — Visible label above the field. Omit → aria-label or aria-labelledby required
requiredboolean — Sets aria-required always; renders the `*` marker only when `label` is also set, since the marker is label-row anatomy (default: false)
infoTooltipReactNode — Info trigger shown next to the label (caller supplies icon/tooltip and its own color/aria-label); only renders when `label` is set, for the same reason
placeholderstring — Placeholder shown when empty
helperTextReactNode — Helper/description text below the field; replaced by `error` when set
errorstring — Error message. String only (no boolean) — sets aria-invalid and error tokens
successboolean — Shows the check-circle indicator (ignored while `error` is set) (default: false)
maxLengthnumber — Character-limit counter, shown as "N/max" in the helper row, also enforced natively
startIconReactNode — Single leading decorative icon, always aria-hidden (no interactive variant)
endIcon{ type: "iconButton"; icon: ReactNode; onClick: React.MouseEventHandler<HTMLButtonElement>; label: string; disabled?: boolean } | { type: "icon"; icon: ReactNode } — Trailing slot; iconButton composes the FDS IconButton (needs an accessible label) and can be disabled on its own while the field stays editable, icon is decorative only. Masked by the state icon in error/success.
endTextstring — Trailing unit or suffix drawn inside the field at its 8px right edge, in the placeholder tone (Figma 7809:25012). Read as the input's description through aria-describedby, never aria-hidden; the value's padding yields to its measured width plus a 4px gap. Not combinable with endIcon or isTypeNumber (dev warning, endText dropped)
isTypeNumberboolean — Turns the field into a number spinbutton with the increment/decrement pair Figma draws, and suppresses the browser's own spinners (RULE-14). Forces type="number", which is what supplies the spinbutton role, ArrowUp/ArrowDown stepping and the aria-value* attributes. Unlike endIcon, the stepper is NOT masked by the error/success indicator — the node draws both (default: false)
stepnumber | string — Increment used by the stepper and by ArrowUp/ArrowDown (default: 1)
minnumber | string — Lower bound; the decrement arrow disables itself once the value reaches it
maxnumber | string — Upper bound; the increment arrow disables itself once the value reaches it
incrementLabelstring — Accessible name of the increment arrow (default: "Increase value")
decrementLabelstring — Accessible name of the decrement arrow (default: "Decrease value")
disabledboolean — Disabled state (default: false)
classNamestring — Additional CSS classes on the outer wrapper

Accessibility

2.1 AAWCAG pass

isTypeNumber (RULE-14): the field is a native type=number, so the spinbutton role, ArrowUp/ArrowDown stepping and aria-valuemin/valuemax/valuenow are the browser's, not re-implemented here. The two drawn arrows are deliberately outside the tab sequence (tabIndex=-1) because the focused input already offers the same action on the keyboard; each keeps an aria-label so it stays named for pointer and browse-mode use. They therefore carry NO focus ring, which is a deliberate deviation from the library's standard ring pattern, declared here as AGENTS.md requires: no keyboard focus can reach them. Each arrow is 16x16 as the node draws it — no target-size minimum applies at WCAG 2.1 AA (2.5.5 is AAA); it is under WCAG 2.2's 2.5.8, which this library does not target. Contrast: the disabled field lost its fill in Sandy's 2026-08-28 edit to node 2682:4410 — it is outlined now, not filled — so the disabled text no longer sits on --bg-input-disabled but on the page itself. That RAISES the pair from 3.69:1 light / 4.35:1 dark, which passed only under WCAG 1.4.3 Note 1's inactive-component exemption, to 5.34:1 light / 6.60:1 dark, which clears 4.5:1 outright. It is therefore DECLARED (`disabled-text`) rather than exempted, as is the border that now carries the state alone (`disabled-border`, 3.72:1 light / 3.25:1 dark). Chip made the identical transition on 2026-08-20 and declares the identical pair. Native <input>, ref forwards directly to it (no wrapper). label uses htmlFor/id association; omitting label requires an aria-label or aria-labelledby — enforced in development with a console.warn (never a throw) if all three are missing, per AGENTS.md's prop contract violation pattern (same mechanism as Dialog's missing-title check). required sets both the visual * and aria-required. error sets aria-invalid and links the message via aria-describedby; helperText is linked the same way — a consumer-provided aria-describedby is merged with (never replaced by) this internal link (space-separated, per WAI-ARIA's multi-id support), while aria-invalid/aria-required stay component-managed and always win over a same-named consumer prop (review: PR #54 round 3). The trailing state icon (error/success) is decorative (aria-hidden) — meaning is carried by text + aria-invalid, not color/icon alone (WCAG 1.4.1). startIcon is always decorative and aria-hidden. endIcon type="iconButton" composes the FDS IconButton and requires an accessible label; type="icon" is decorative. Focus uses the standard FDS ring (ring-focus + ring-offset-focus), matching the Figma Focus state exactly (box-shadow ring in Figma). Disabled uses native disabled (removed from tab order); its low-contrast tokens are exempt under WCAG 1.4.3 Note 1. Resolved (2026-07-30 Figma token export): the success check-circle icon (--color-feedback-success-primary) now uses a mode-specific value — --green-600 #17ab1f in dark mode (5.23:1 on --bg-input-default #13213e) and --green-700 #117916 in light mode (4.42:1 on --bg-input-default #e4e5e7) — so both modes clear the 3:1 AA non-text threshold (WCAG 1.4.11). This is the mode-specific-override resolution previously anticipated for this state; the earlier light-mode failure (green-600 #17ab1f = 2.42:1) no longer exists. All declared pairs pass; verified by pnpm check:contrast.