Masked Input
Text field that enforces a character mask as you type — dates, card numbers, phone numbers, license keys. The mask’s literals are typed for the user; value holds only the raw characters, and the raw value is what forms receive, so the mask stays presentation.
<arc-masked-input> Overview
>
MaskedInput is a single-line text field that formats fixed-shape values while the user types them. The `mask` prop describes the shape with four slot characters — `#` for a digit, `A` for an uppercase letter (lowercase input is uppercased), `a` for any letter, and `*` for a letter or digit — and every other character is a literal that the component types for the user. A date mask of `##/##/####` means the user types eight digits and the slashes appear on their own, with the caret always landing on the next position that can accept a character.
The decision that makes the component composable: `value` holds only the raw characters, never the mask's literals. A completed date field reports `12042026`, and that raw string is also what the field submits with a form. The formatted string — `12/04/2026` — is presentation, available read-only as `formattedValue` and in the `formatted` key of every event detail. This means switching a card mask from space-grouped to dash-grouped changes nothing downstream, and your backend never has to strip formatting it did not ask for.
Editing behaves the way users expect from a good mask. Typing inserts at the caret and skips literals forward; Backspace deletes the previous fillable character, skipping literals backward; pasting strips non-conforming characters and fills the remaining positions, so a card number pasted with dashes lands cleanly in a space-grouped mask. A character that does not fit its slot is silently rejected and the caret stays put. Before typing begins, the native placeholder shows the mask shape; once typing starts, the unfilled remainder renders in the field as a muted hint, such as `12/__/____`.
MaskedInput follows the v3 commit contract: `arc-input` fires on each accepted edit, and `arc-change` fires on blur or Enter when the value changed — and immediately when the last mask position fills, the same fixed-length commit that OTP Input established. Constraint validation is built in: a required empty field fails with `valueMissing`, and a partially filled mask fails with an "Incomplete value" pattern error, so a half-typed card number cannot pass a form's validation.Guidelines
When to use
- Use a mask when the value has one fixed, well-known shape: dates, card numbers, phone numbers in a single locale, license or serial keys
- Read `value` (or the form submission) for storage and `formattedValue` only for display — the raw value is the contract
- Listen for `arc-change` to validate or submit — it fires the moment the mask completes, so users need not leave the field first
- Set `autocomplete` to the matching token (for example `cc-number` on a card field) so browser autofill keeps working
- Always provide a `label`; the mask shape in the placeholder is a hint, not a name for the field
- Prefer `A` over `a` for license and product keys so the stored value is case-normalized without the user caring
When not to use
- Do not mask free-form values like names, email addresses, or search queries — a mask that fights variable-length input is worse than no mask; use Input instead
- Do not use MaskedInput for short fixed-length verification codes — OTP Input and Pin Input give each character its own box and auto-advance
- Do not mask international phone numbers with a single pattern — number lengths vary by country, and a wrong mask locks users out of entering their own number
- Do not parse `formattedValue` on the server — submit and store the raw value, and format at the display edge
- Do not use the mask as a substitute for validation of meaning — `##/##/####` accepts 99/99/9999; check that a date is real before accepting it
Features
- Declarative mask pattern: `#` digit, `A` uppercase letter, `a` any letter, `*` alphanumeric, everything else a literal
- Raw `value` with no literals — the formatted string is exposed separately as read-only `formattedValue`
- Forms receive the raw value, so the presentation format never leaks into submitted data
- Literal skipping in both directions: typing jumps forward past literals, Backspace deletes through them
- Paste support that strips non-conforming characters and fills the remaining positions
- Muted in-field hint for unfilled positions once typing starts, configurable via `placeholder-char`
- Fires `arc-input` per accepted edit and `arc-change` on blur, Enter, or the moment the mask completes
- Built-in validation: required-empty is `valueMissing`, a partial fill is an "Incomplete value" pattern error
- Numeric masks automatically request the numeric keyboard on mobile
- Prefix and suffix slots, label, sizes, and disabled/readonly states matching Input
Preview
Usage
This component requires JavaScript. No pure HTML/CSS version is available — use the Web Component directly or a framework wrapper.
<!-- Date, card, and license-key masks -->
<div style="display:flex; flex-direction:column; width:100%; max-width:400px; gap:16px;">
<arc-masked-input label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp"></arc-masked-input>
<arc-masked-input label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number"></arc-masked-input>
<arc-masked-input label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•"></arc-masked-input>
</div>
<script>
const card = document.querySelector('[name="card"]');
card.addEventListener('arc-change', (e) => {
// e.detail.value is the raw digits; e.detail.formatted is presentation.
console.log(e.detail.value, e.detail.formatted);
});
</script> import { MaskedInput } from '@arclux/arc-ui-react';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: 400, gap: 16 }}>
<MaskedInput label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp" />
<MaskedInput
label="Card number"
name="card"
mask="#### #### #### ####"
autocomplete="cc-number"
onArcChange={(e) => console.log(e.detail.value)}
/>
<MaskedInput label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•" />
</div>
);
} <script setup>
import { MaskedInput } from '@arclux/arc-ui-vue';
</script>
<template>
<div style="display:flex; flex-direction:column; width:100%; max-width:400px; gap:16px;">
<MaskedInput label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp" />
<MaskedInput label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number" />
<MaskedInput label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•" />
</div>
</template> <script>
import { MaskedInput } from '@arclux/arc-ui-svelte';
</script>
<div style="display:flex; flex-direction:column; width:100%; max-width:400px; gap:16px;">
<MaskedInput label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp" />
<MaskedInput label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number" />
<MaskedInput label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•" />
</div> import { Component } from '@angular/core';
import { MaskedInput } from '@arclux/arc-ui-angular';
@Component({
imports: [MaskedInput],
template: `
<div style="display:flex; flex-direction:column; width:100%; max-width:400px; gap:16px;">
<arc-masked-input label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp"></arc-masked-input>
<arc-masked-input label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number"></arc-masked-input>
<arc-masked-input label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•"></arc-masked-input>
</div>
`,
})
export class PaymentFormComponent {} import { MaskedInput } from '@arclux/arc-ui-solid';
export default function Example() {
return (
<div style={{ display: 'flex', 'flex-direction': 'column', width: '100%', 'max-width': '400px', gap: '16px' }}>
<MaskedInput label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp" />
<MaskedInput label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number" />
<MaskedInput label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•" />
</div>
);
} import { MaskedInput } from '@arclux/arc-ui-preact';
export default function Example() {
return (
<div style={{ display: 'flex', flexDirection: 'column', width: '100%', maxWidth: 400, gap: 16 }}>
<MaskedInput label="Expiry date" name="expiry" mask="##/##/####" autocomplete="cc-exp" />
<MaskedInput label="Card number" name="card" mask="#### #### #### ####" autocomplete="cc-number" />
<MaskedInput label="License key" name="license" mask="AAA-###-AAA" placeholder-char="•" />
</div>
);
} API
-
autoValidatesbooleanfalse - Runs its own constraint logic — owns the whole validity flag set.
-
formattedValue - The formatted presentation string — raw characters interleaved with mask literals, e.g. raw 12042026 under a date mask reads 12/04/2026. Read-only: it is derived from value and mask, never stored, and never submitted.
-
maskstring'' - The mask pattern. `#` accepts a digit, `A` an uppercase letter (lowercase input is uppercased), `a` any letter, `*` a letter or digit; every other character is a literal typed for the user. Examples: `##/##/####`, `#### #### #### ####`, `AAA-###`.
-
valuestring'' - The RAW accepted characters only, with no mask literals — `12042026`, never `12/04/2026`. The formatted string is presentation; read it from `formattedValue`. Programmatic values are conformed against the mask, so setting a formatted string keeps only the characters the mask accepts.
-
placeholder-charstring'_' - Character rendered in unfilled positions of the in-field hint once typing starts (for example `12/__/____`). Before any input, the native placeholder shows the full mask shape. Defaults to `_`.
-
labelstring'' - Visible label rendered above the field. Automatically associated with the field via a generated id, ensuring screen readers announce it correctly.
-
namestring'' - The `name` attribute sent with form data on submission. The submitted value is the RAW value, without mask literals.
-
disabledbooleanfalse - Prevents user interaction and applies a muted visual treatment. The field value is excluded from form submission when disabled.
-
requiredbooleanfalse - Marks the field as required. An empty field fails validation with valueMissing; a partially filled one fails with an "Incomplete value" pattern error.
-
autocompletestring'' - Passed through to the inner input, e.g. `cc-number` on a card field so browser autofill can offer saved cards.
-
errorstring'' - Error message displayed below the input. When set, the input border turns red and the error text appears.
-
size'sm' | 'md' | 'lg''md' - Controls the input size. Options: 'sm', 'md', 'lg'.
-
readonlybooleanfalse - Prevents the user from editing the value while keeping the field focusable, and the value is still submitted with the form.
-
formAssociatedbooleantrue -
propertiesobject{ required: { type: Boolean, reflect: true }, readonly: { type: Boolean, reflect: true }, } - Lit merges static properties up the prototype chain, so every consumer gets these without declaring them. `required` participates in constraint validation below; `readonly` reflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state).
-
form -
validity -
validationMessage
Events
-
arc-inputdetail: { value: string, formatted: string } - Fired on each accepted edit. `value` is the raw characters; `formatted` is the presentation string. A rejected character fires nothing.
-
arc-changedetail: { value: string, formatted: string } - Fired on blur or Enter when the value changed, and immediately when the last mask position fills — a complete mask is a committed value, the fixed-length precedent set by OTP Input.
See Also
- Input Versatile form control supporting single-line text, email, password, and multiline textarea modes with built-in label, placeholder, and validation states. Pairs with Form for complete data-entry workflows.
- OTP Input A one-time password input that renders a row of individual character boxes with auto-advance, paste support, and configurable length and input type.
- Pin Input One-character-per-box input for PINs, OTPs, and verification codes with auto-advance, paste support, and optional masking.
- Number Input A numeric stepper input with decrement and increment buttons flanking a central text field, supporting min/max clamping, step increments, and keyboard shortcuts.
- Form Form wrapper with built-in validation, error aggregation, and submit handling. Composes Input, Textarea, and Button into a cohesive data-entry workflow.