# TextInput

> The text field: a white box with a tinted edge, a brand halo on focus, adornments and a password reveal.

Category: Forms. Docs page: https://rime.mjsons.net/components/text-input

## Installation

```bash
npx shadcn@latest add KezLahd/rime/text-input
```

TextInput is the one box every text-like field wears: Textarea, Select's field trigger, DateField, SearchField and PrefixedInput all share its --input-* tokens, so one theme change moves them together.

Focus is drawn on the whole box, adornments included (a leading icon, a trailing unit, the reveal button), never on the bare <input>. Invalid warms the fill and turns the edge to the danger hue; read-only is a dashed sunken box; disabled is flat and sunken.

It is a plain controlled or uncontrolled <input>: every native attribute passes through (type, autoComplete, inputMode, maxLength).

## Usage

```tsx
import { Field, TextInput } from "@/components/ui";

<Field label="Email" hint="We send the sign-in link here.">
  <TextInput type="email" autoComplete="email" value={email} onChange={(e) => setEmail(e.target.value)} />
</Field>
```

## Props: TextInput

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `leadingIcon` | `ReactNode` |  |  |
| `trailing` | `ReactNode` |  | Right-hand slot inside the box: a unit ("%"), a clear button. |
| `mono` | `boolean` |  | Monospace with slight tracking, for ID numbers, references, postcodes. |
| `invalid` | `boolean` |  | Overrides the Field's invalid state. |
| `revealable` | `boolean` |  | Adds a show/hide toggle. Only meaningful with type="password". |
| `defaultRevealed` | `boolean` | `false` | Starts with the password readable. For the styleguide and review screens. |
| `size` | `"sm" \| "md"` | `"md"` |  |
| `boxClassName` | `string` |  | Class for the outer box rather than the <input>. |

Also accepts the native attributes of: AriaAttributes, DOMAttributes, HTMLAttributes, InputHTMLAttributes.

## Props: Field

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `label` (required) | `ReactNode` |  |  |
| `hint` | `ReactNode` |  |  |
| `error` | `ReactNode` |  | Presence marks the control invalid. Say what to do, not just what is wrong. |
| `required` | `boolean` | `false` |  |
| `optional` | `boolean` |  | Shows "(optional)". Use on forms where most fields are required. |
| `id` | `string` |  | Pass to link a field to an ErrorSummary entry. Generated otherwise. |
| `group` | `boolean` |  | fieldset + legend, for RadioGroup, a checkbox list, OtpInput, DateField. |
| `labelStyle` | `"sentence" \| "caps"` | `"sentence"` | sentence = 13px bold (default, reads well for long labels). caps = the reference 11.5px uppercase micro-label, for short auth/admin forms. |
| `labelAction` | `ReactNode` |  | Something on the label's right edge, e.g. a "Forgot password?" link. |
| `hideLabel` | `boolean` |  |  |
| `className` | `string` |  |  |
| `children` (required) | `ReactNode` |  |  |

## Examples

### In a Field

Label, hint and error come from Field; the input reads them from context.

```tsx
<Field label="Project name" hint="Shown to everyone on the team.">
  <TextInput placeholder="e.g. Atlas redesign" />
</Field>
```

### Adornments and reveal

```tsx
<TextInput leadingIcon={<IconSearch size={16} />} placeholder="Search" />
<TextInput trailing="kg" inputMode="decimal" />
<TextInput type="password" revealable autoComplete="current-password" />
```

### States

```tsx
<Field label="Postcode" error="Enter a 4-digit postcode.">
  <TextInput invalid defaultValue="20" />
</Field>
<TextInput readOnly defaultValue="INV-1042" mono />
<TextInput disabled placeholder="Disabled" />
<TextInput size="sm" placeholder="Small" />
```

## Theme tokens

Per-component tokens. Undeclared by default: the default below is the fallback the CSS uses. Set any of them at :root or on a wrapper to restyle every instance.

| Token | Default |
| --- | --- |
| `--field-focus-border` | `rgba(var(--rgb-brand), 0.7)` / `var(--focus-color)` |
| `--field-focus-ring` | `var(--focus-halo)` |
| `--field-focus-ring-invalid` | `0 0 0 4px rgba(var(--rgb-danger), 0.14)` |
| `--field-hover-border` | `rgba(var(--rgb-brand-deep), 0.3)` |
| `--input-bg` | `var(--surface-solid)` |
| `--input-border` | `var(--line-input)` |
| `--input-border-width` | `var(--border-w-field)` |
| `--input-font-size` | `var(--text-input)` |
| `--input-height-md` | `var(--field-h-md)` |
| `--input-height-sm` | `var(--field-h-sm)` |
| `--input-pad-x` | `calc(14px * var(--density))` |
| `--input-radius` | `var(--r-md)` / `var(--r-sm)` |
| `--input-shadow` | `var(--shadow-input)` |

System tokens it reads: `--border-w-field`, `--danger-base`, `--danger-field-fill`, `--danger-ink`, `--density`, `--dur`, `--ease`, `--field-h-md`, `--field-h-sm`, `--focus-color`, `--focus-halo`, `--font-body`, `--font-mono`, `--ink-body`, `--ink-brand`, `--ink-disabled`, `--ink-muted`, `--ink-placeholder`, `--ink-secondary`, `--line-input`, `--r-md`, `--r-sm`, `--r-xs`, `--rgb-brand`, `--rgb-brand-deep`, `--rgb-danger`, `--shadow-input`, `--surface-solid`, `--surface-sunken`, `--text-body`, `--text-caption`, `--text-input`, `--text-small`, `--wash-subtle`.

## Accessibility

- Wrap it in <Field label=…>: Field wires the label, hint and error to the control (id, aria-describedby, aria-invalid) through context, so the control needs no ids of its own.
- Placeholder ink holds 4.97:1 on the white box; never put instructions only in a placeholder.
- revealable adds a real button inside the box whose name switches between "Show password" and "Hide password" (no aria-pressed, which would read as "Hide password, pressed").
- Set autoComplete on every personal-data field (WCAG 1.3.5).

## Do

- Size the box to the content where the content is short (a postcode), with a className width.

## Don't

- Don't style the <input> itself; theme the box with --input-* tokens.
- Don't show an error before the user has left the field or submitted.

Related: https://rime.mjsons.net/components/select, https://rime.mjsons.net/components/textarea, https://rime.mjsons.net/components/search-field
