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). 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.
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 Pin Input uses. 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. Pin Input gives each character its own box and auto-advances
- 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 and 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,Aan uppercase letter (lowercase input is uppercased),aany 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, never12/04/2026. The formatted string is presentation; read it fromformattedValue. 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
nameattribute 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.
autocompletestring''- Passed through to the inner input, e.g.
cc-numberon 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.
requiredbooleanfalse- Marks the field as required. An empty field fails validation with valueMissing; a partially filled one fails with an "Incomplete value" pattern error.
readonlybooleanfalse- Prevents the user from editing the value while keeping the field focusable, and the value is still submitted with the form.
size'sm' | 'md' | 'lg''md'- Controls the input size. Options: 'sm', 'md', 'lg'.
formAssociatedbooleantruepropertiesobject{ // flag(), unlike `disabled`. The exclusion in props.js is specifically // about form-associated *platform* semantics: a `disabled` content // attribute that is merely present makes the element actually disabled // per the HTML spec, and formDisabledCallback assigns the property back, // so no converter can win. Neither of these is platform-mapped: // `required` is enforced by _computeValidity() below and `readonly` by // each component's own interaction handlers, so the stock converter buys // nothing here and costs the usual bug: `required="false"` read as true, // blocking submission of a form the author meant to leave optional. // Finding #48's shape, across all 26 form controls at once. required: flag(false), readonly: flag(false), }- Lit merges static properties up the prototype chain, so every consumer
gets these without declaring them.
requiredparticipates in constraint validation below;readonlyreflects for styling and is enforced by each component's interaction handlers (the mixin can't know which gestures mutate state). formvalidityvalidationMessage
Methods
checkValidity()boolean- Whether the control currently satisfies its constraints, per the native
constraint-validation API. Fires
invalidon the element when it does not, and reports nothing to the user. reportValidity()boolean- As checkValidity(), but also shows the browser's validation message against the control when it fails.
Events
arc-inputdetail:{ value: string, formatted: string }- Fired on each accepted edit.
valueis the raw characters;formattedis 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
- InputVersatile 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.
- Pin InputOne-character-per-box input for PINs, OTPs, and verification codes with auto-advance, paste support, and optional masking.
- Number InputA numeric stepper input with decrement and increment buttons flanking a central text field, supporting min/max clamping, step increments, and keyboard shortcuts.
- FormForm wrapper with built-in validation, error aggregation, and submit handling. Composes Input, Textarea, and Button into a cohesive data-entry workflow.