ComponentsForm
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"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.
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.
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.
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.
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.
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.
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.
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.
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.
Invalid
aria-invalid draws the error edge; aria-describedby reads the message with the field.
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, withinputmode="numeric"when every slot takes a digit. - Labels
- Name it with a
Labeloraria-label. A placeholder showing the pattern is not a name; say the format in the label or in a hint read througharia-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;
arefuses accented letters.
Keyboard
| Key | Behaviour |
|---|---|
| 0–9orA–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. |
| CtrlV | Pastes text as if typed: the fixed characters and anything that fits no slot are dropped. |
API
InputMask
| Prop | Type | Default | Description |
|---|---|---|---|
maskrequired | 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.
The tokens its classes read. Change their values in your theme, and it follows.
| Token | Used for |
|---|