Masked input — accessibility
Keyboard
- Native
<input>: typing edits the value in place, Tab moves focus in and out;disabledremoves it from the tab order (same asinput). There is no keyboard shortcut and nothing to escape out of — the component renders no popover or calendar. - Backspace over an auto-inserted separator deletes the digit before it too (
maskInput), so deleting never gets stuck on a character the user did not type — same behaviour asmasked-date-input, covered byfield-masks.test.ts. - No arrow-key segment navigation; the caret moves through the mask like an ordinary text field, only separator characters are special-cased by the template.
inputModeis set per mask (numericfor IČO/PSČ/rodné číslo,textfor DIČ), so mobile keyboards match the expected characters.
ARIA
- Accessible name: pass
ariaLabel, rendered as a nativearia-labelon the<input>— the same limitation asinputapplies when the component is used inside<Field>/<DrawerField>, whose<label>is not linked to its control viahtmlFor/id(seetext-field/input.a11y.md).MaskedInputdoes not add that link either; callers must supplyariaLabelthemselves, and every current call site does. aria-invalidis set only wheninvalidis true (aria-invalid={invalid || undefined}): a valid or empty field carries noaria-invalidattribute at all rather than"false"— same convention asmasked-date-input.- Error linking, unlike
masked-date-input(which has nodescribedByprop at all):MaskedInputacceptsdescribedByand wires it toaria-describedbyon the input (aria-describedby={describedBy ?? fieldDescribedBy}). - The
formlesscaller (NewContactDialog, rodné číslo) passesdescribedBy={rcErrId}and gives the rendered<span class="fl-err">that sameid— explicitly linked, as before. - The
fieldcallers (FirmyPage, IČO/DIČ/PSČ inside<DrawerField>) still do not passdescribedByexplicitly, but as of acrm MR !4085 (28 Sep 2026, "fix(ui): chyba pod DS Field se čtečce provází sama") they no longer need to:MaskedInputreadsuseFieldDescribedBy()and falls back to it, and<Field>/<DrawerField>now give the rendered error (or hint) span an automaticidand pass it down through that context. A screen reader user is now told both that the field is invalid (aria-invalid) and why (the linked error text), with no per-call-site wiring — seetext-field/field.a11y.md's ARIA section for the mechanism. - The ghost span (
.acrm-mask-ghost/.fl-mask-ghost) isaria-hidden="true"in both variants — it visually echoes the typed value plus the remaining template; exposing it would read the value twice.
Focus
fieldvariant: focus is drawn by the surrounding.acrm-inputwrap(:focus-within), same ring asinput—border-color: var(--accent)+ 3px ring at 16%--accent..is-errorkeeps the same geometry with--critical; a focused error field shows the accent ring (focus rule wins by source order, same asinput).formlessvariant: the input itself has no border (.fl-mask-inis transparent and absolutely positioned over the ghost), so it relies on the row's own focus treatment (.fl-row:has(.fl-mask-in:focus)tints the row) rather than drawing a dedicated ring — same asmasked-date-input.
Contrast
Computed from packages/ui/src/tokens/tokens.css (WCAG 2.x relative luminance) — the same token set already measured in text-field/input.a11y.md and masked-date-input.a11y.md:
- Typed value (valid)
--fgon--surface-100— 17.59:1 light / 13.09:1 dark (fieldvariant,--surface-50in dark); 17.59:1 light / 14.53:1 dark (formlessvariant, dialog body surface). - Typed value (invalid)
--critical-ink— 5.37:1 / 5.80:1, passes AA in both variants (same token asmasked-date-input's invalid state). - Ghost
.rest--fg-muted— 2.85:1 light / 3.27–3.63:1 dark, below AA 4.5:1 for text; it is a template echo, not content, same system-wide--fg-mutedlimit as every other muted placeholder/ghost in this family. - Focus border
--accent— 4.44:1 light / 3.83:1 dark, passes the 3:1 non-text minimum. - Error border
--critical(fieldvariant only) — 3.91:1 light / 4.35:1 dark, passes 3:1. - Resting hairline
--border-strong(fieldvariant only) — 1.38:1 light / 1.55:1 dark, hairline, below the 3:1 non-text minimum — same as every.acrm-inputwrapat rest.
Known limits
- ~~
describedBy/aria-describedbylinking depends on the caller wiring it — one call site does (NewContactDialog), the drawer call sites do not (FirmyPage).~~ Fixed in acrm MR !4085 (28 Sep 2026) viauseFieldDescribedBy(): every call site now getsaria-describedbyfor free from the enclosing<Field>/<DrawerField>unless it passes its owndescribedBy— see ARIA above. <Field>/<DrawerField>do not link label and control — same gap asinput; the accessible name must be supplied by the screen viaariaLabel.- No live region and no "character rejected" feedback: a dropped keystroke (a letter typed into a numeric-only slot) simply does not appear, with nothing announced to a screen reader user — same limit as
masked-date-input. - The ghost's ARIA-hidden echo means the visual template ("________" for an empty IČO) is not itself announced; a screen reader user only hears the label and, once typed, the value — intended, but worth knowing if a future caller wants the template read aloud.
Source: docs/text-field/masked-input.a11y.md · GitLab