# Input mask

A text field that keeps a pattern, such as a phone number or a date, and fills its slots as people type.

- **Import:** `import { InputMask } from "@booleanpress/ui/input-mask"`
- **Page:** <https://ui.booleanpress.com/components/input-mask> · @booleanpress/ui 0.2.0

## Usage

`InputMask` is the library's `Input` with a mask. `mask` is the pattern: `9` takes a digit, `a` a letter (A–Z), `*` a letter or a digit, and every other character is fixed and typed for you. Name it with a `Label` whose `htmlFor` is its `id`.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export function SupportPhone() {
  return (
    <>
      <Label htmlFor="support-phone">Support phone</Label>
      <InputMask id="support-phone" type="tel" mask="(999) 999-9999" placeholder="(999) 999-9999" />
    </>
  )
}
```

Typing fills the next slot and skips the fixed characters; a character that does not fit its slot is refused. Backspace and Delete remove a character and move the rest back. Pasted text is read as if typed, so `555-123-4567` fills `(555) 123-4567`. Empty slots show `slotChar` (`_`), or a string as long as the pattern, such as `mm/dd/yyyy`. Everything after `?` is optional. When the field loses focus with the required part unfinished, `autoClear` (on by default) empties it; `autoClear={false}` keeps what was typed.

It is uncontrolled with `defaultValue`, or controlled with `value` and `onValueChange`, which is called with the new value and `{ complete }`. The value is what the field shows, `(555) 123-4567`; with `unmask` it is the typed characters only, `5551234567`, in `value`, `defaultValue` and `onValueChange` alike. `onChange` still receives the input's event, with the text it shows. A field whose slots are all digits opens the number pad on phones. With `name`, the form submits the text the field shows, mask included, even with `unmask`; a reset of the form puts an uncontrolled field back to its `defaultValue`. It takes the other props of `Input`: `size`, `variant`, `clearable`, `aria-invalid`.

## Examples

### Basic

A phone number: the brackets, the space and the dash are typed for you.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskBasic() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="support-phone">Support phone</Label>
      <InputMask id="support-phone" type="tel" mask="(999) 999-9999" placeholder="(999) 999-9999" />
    </div>
  )
}
```

### Patterns

A date, a licence key mixing letters and digits, and a tax number.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskPatterns() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label htmlFor="invoice-date">Invoice date</Label>
        <InputMask id="invoice-date" mask="99/99/9999" placeholder="99/99/9999" />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="licence-key">Licence key</Label>
        <InputMask id="licence-key" mask="a*-999-a999" placeholder="a*-999-a999" />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="tax-number">Tax number</Label>
        <InputMask id="tax-number" mask="999-99-9999" placeholder="999-99-9999" />
      </div>
    </div>
  )
}
```

### Optional part

Everything after `?` is optional: the number is complete without the extension.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskOptional() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="office-phone">Office phone, extension optional</Label>
      <InputMask id="office-phone" type="tel" mask="(999) 999-9999? x99999" placeholder="(999) 999-9999? x99999" />
    </div>
  )
}
```

### Slot character

`slotChar="mm/dd/yyyy"` shows what each empty slot is for.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskSlotCharacter() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="renewal-date">Renewal date</Label>
      <InputMask id="renewal-date" mask="99/99/9999" slotChar="mm/dd/yyyy" placeholder="mm/dd/yyyy" />
    </div>
  )
}
```

### Unmasked value

With `unmask`, `onValueChange` reports the digits only, while the field shows the mask.

```tsx
import { useState } from "react"
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskUnmask() {
  const [raw, setRaw] = useState("")
  const [masked, setMasked] = useState("")

  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="sms-number">SMS alerts number</Label>
      <InputMask
        id="sms-number"
        type="tel"
        mask="(999) 999-9999"
        placeholder="(999) 999-9999"
        unmask
        onValueChange={setRaw}
        onChange={(event) => setMasked(event.target.value)}
      />
      <dl className="grid grid-cols-[auto_1fr] gap-x-3 text-sm text-muted-foreground">
        <dt>Value</dt>
        <dd className="font-mono text-foreground">{raw || "—"}</dd>
        <dt>Shown</dt>
        <dd className="font-mono text-foreground">{masked || "—"}</dd>
      </dl>
    </div>
  )
}
```

### Auto-clear

By default an unfinished number is cleared when the field loses focus; `autoClear={false}` keeps it.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskAutoClear() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <div className="flex flex-col gap-2">
        <Label htmlFor="billing-phone">Billing phone, cleared when unfinished</Label>
        <InputMask id="billing-phone" type="tel" mask="(999) 999-9999" placeholder="(999) 999-9999" />
      </div>
      <div className="flex flex-col gap-2">
        <Label htmlFor="backup-phone">Backup phone, kept when unfinished</Label>
        <InputMask
          id="backup-phone"
          type="tel"
          mask="(999) 999-9999"
          placeholder="(999) 999-9999"
          autoClear={false}
          defaultValue="555"
        />
      </div>
    </div>
  )
}
```

### Sizes

`sm` is 28 px tall with 12 px text, `default` 35 px with 14 px, `lg` 42 px with 16 px.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"

export default function InputMaskSizes() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-4">
      <InputMask size="sm" mask="99/99/9999" aria-label="Small" placeholder="Small" />
      <InputMask mask="99/99/9999" aria-label="Normal" placeholder="Normal" />
      <InputMask size="lg" mask="99/99/9999" aria-label="Large" placeholder="Large" />
    </div>
  )
}
```

### Disabled

The masked value shows, and cannot be changed or submitted.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"

export default function InputMaskDisabled() {
  return (
    <div className="w-full max-w-xs">
      <InputMask disabled mask="(999) 999-9999" aria-label="Support phone" defaultValue="5551234567" />
    </div>
  )
}
```

### Invalid

`aria-invalid` draws the error edge; `aria-describedby` reads the message with the field.

```tsx
import { InputMask } from "@booleanpress/ui/input-mask"
import { Label } from "@booleanpress/ui/label"

export default function InputMaskInvalid() {
  return (
    <div className="flex w-full max-w-xs flex-col gap-2">
      <Label htmlFor="invalid-phone">Support phone</Label>
      <InputMask
        id="invalid-phone"
        type="tel"
        mask="(999) 999-9999"
        placeholder="(999) 999-9999"
        aria-invalid
        aria-describedby="invalid-phone-error"
      />
      <p id="invalid-phone-error" className="text-sm text-destructive-strong">
        Enter all ten digits of the number.
      </p>
    </div>
  )
}
```

## Accessibility

**Semantics.** A native `input`, with `inputmode="numeric"` when every slot takes a digit.

**Labels.** Name it with a `Label` or `aria-label`. A placeholder showing the pattern is not a name; say the format in the label or in a hint read through `aria-describedby`.

**Focus.** One tab stop. Focus turns the edge `--ring`. Focusing an unfinished value puts the caret at its first empty slot.

**Known limits.**

- Undo and redo (⌘Z, ⇧⌘Z, the Edit menu) do nothing in a masked field: the mask rewrites the text on every keystroke, so the browser's history would put back half-masked text.
- A refused character is not announced: say the format in the label or a hint.
- The fixed characters and empty slots are part of the text, so a screen reader reads them as it reads any text.
- Text composed with an input method editor is read once it is committed.
- A slot letter is A–Z; `a` refuses accented letters.

### Keyboard

| Key | Behaviour |
| --- | --- |
| 0–9 or A–Z | Fills the next slot when the character fits it, skipping the fixed characters; otherwise it is refused. |
| Backspace | Removes the character before the caret; the characters after it move back. |
| Delete | Removes the character after the caret; the characters after it move back. |
| Ctrl + V | Pastes text as if typed: the fixed characters and anything that fits no slot are dropped. |

## API

### InputMask

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `mask` (required) | `string` |  | The pattern: `9` a digit, `a` a letter, `*` a letter or a digit, `?` starts the optional part; anything else is fixed. |
| `autoClear` | `boolean` | `true` | Empties the field when it loses focus with the required part unfinished. |
| `clearable` | `boolean` |  | Shows a button that empties the field while it has a value. |
| `defaultValue` | `string` |  | The starting value of an uncontrolled field, in the same form as `value`. |
| `onValueChange` | `((value: string, details: { complete: boolean; }) => void)` |  | Called with the new value on every change, and whether the required part is filled. |
| `size` | `"default" \| "sm" \| "lg"` |  | The field's size: 28, 35 or 42 px tall. Defaults to the provider's `controlSize`. |
| `slotChar` | `string` | `_` | What an empty slot shows: one character for every slot, or a string as long as the pattern (`mm/dd/yyyy`). |
| `unmask` | `boolean` | `false` | `value`, `defaultValue` and `onValueChange` carry the typed characters only (`5551234567`), not the mask. |
| `value` | `string` |  | The value, as the field shows it, or the typed characters only with `unmask`. |
| `variant` | `"default" \| "filled"` |  | `filled` draws the grey `--field-filled` fill. Defaults to the provider's `fieldVariant`. |

Every part takes `className`, merged with its defaults by `cn()`, and `ref`, which reaches the element it renders.

**Data attributes:** `data-slot="input-mask"` (InputMask), and `data-mask`.

## Theming

The field is `Input`'s: `--field`, `--control`, `--ring`, `--invalid` and `--field-disabled`.
