<!-- Protocore Design System — MaskInput -->
# MaskInput

- **Category:** Inputs (`inputs`)
- **Slug:** `inputs/mask-input`
- **Status:** stable
- **Platforms:** web
- **Import:** `import { MaskInput } from "@protocore/pds";`
- **Docs:** https://pds.protocore.io/components/inputs/mask-input

> An Input that formats typing against a fixed mask (phone, card, date), owning the raw characters and displaying the masked string.

## When to use it

Use **MaskInput** for fixed-shape values that read better with separators — phone numbers, card numbers, dates, postal codes. For free numeric entry with steppers use **NumberInput**; for a one-time code use **PinInput** / **OTPInput**; for calendar-backed dates use **DateInput**. A mask formats input — it does not guarantee the value is real; validate on submit.

## Props

| Prop | Type | Required | Default | Description |
| --- | --- | --- | --- | --- |
| `className` | `string` | no | — | — |
| `defaultValue` | `string` | no | — | Initial raw value when uncontrolled. |
| `invalid` | `boolean` | no | — | Mark the field invalid — danger border plus `aria-invalid`. |
| `mask` | `string` | yes | — | The format mask — `#` digit, `A` letter, `*` alphanumeric; other chars are literals. |
| `onValueChange` | `((raw: string, masked: string) => void)` | no | — | Fires with the next raw value and its masked rendering. |
| `size` | `enum` | no | `md` | Control height: `sm` (32) · `md` (36, default) · `lg` (40). |
| `style` | `CSSProperties` | no | — | — |
| `value` | `string` | no | — | Controlled raw value (significant characters only). Pair with `onValueChange`. |

## Examples

### Basics

Pass a `mask` string — `#` is a digit, `A` a letter, `*` alphanumeric, and every other character is a literal shown in place. The component inserts literals as you type.

```tsx
import { MaskInput } from "@protocore/pds";

export default function Demo() {
  return (
    <div style={{ maxWidth: 260 }}>
      <MaskInput mask="(###) ###-####" aria-label="Phone" placeholder="(___) ___-____" />
    </div>
  );
}
```

### Raw + masked value

`onValueChange(raw, masked)` reports both the significant characters and their formatted rendering. Store `raw`; show `masked`. The control is controllable by its raw value.

```tsx
import { useState } from "react";
import { MaskInput, Text, Stack } from "@protocore/pds";

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

  return (
    <Stack gap={2} style={{ maxWidth: 300 }}>
      <MaskInput
        mask="#### #### #### ####"
        aria-label="Card number"
        placeholder="0000 0000 0000 0000"
        onValueChange={(r, m) => {
          setRaw(r);
          setMasked(m);
        }}
      />
      <Text size="sm" color="muted" mono>
        raw: {raw || "—"} · masked: {masked || "—"}
      </Text>
    </Stack>
  );
}
```

## Do & don't

**Do**

- Persist the `raw` value; render the `masked` one.
- Give it an `aria-label` or wrap it in a `Field`.
- Match the mask to the real format your backend expects.

**Don't**

- Don't treat a full mask as validation — a well-formed phone number can still be wrong.
- Don't use a mask for open-ended text — it drops non-matching characters.
- Don't mask secret one-time codes — use PinInput/OTPInput.

## Accessibility

**Notes**

- Renders a standard text input, so it inherits the Input focus ring and `aria-invalid`.
- `inputMode` is inferred (`numeric` for digit-only masks) to surface the right mobile keypad.
- Non-matching characters are ignored rather than silently accepted, keeping the display honest.

## Related

`input`, `number-input`, `pin-input`, `date-input`

---

© Protocore. All rights reserved. Use of the Protocore Design System requires prior written authorization from Protocore (contact@protocore.io). These machine-readable materials must not be ingested into ML-training datasets or derivative design systems. See https://pds.protocore.io/legal/
