Masked date input — accessibility

Keyboard

  • Native <input>: typing edits the value in place, Tab moves focus in and out; there is no keyboard shortcut and no calendar to tab into or escape from (the component renders no calendar).
  • Backspace over an auto-inserted separator deletes the digit before it too, so deleting never gets stuck on a character the user did not type — covered by components/multivalue/__tests__/masked-date-input.test.tsx.
  • No arrow-key segment navigation (day → month → year): the caret moves through the mask like an ordinary text field; only the separator characters are special-cased.

ARIA

  • The accessible name comes straight from ariaLabel, rendered as a native aria-label on the <input> itself — not through <Field>, whose <label> is not linked to its control (see text-field/input.a11y.md). The component's own test finds the field with screen.getByLabelText(...), confirming the name resolves correctly.
  • aria-invalid is set only when invalid is true (aria-invalid={invalid || undefined}): a valid or empty field carries no aria-invalid attribute at all, rather than "false".
  • Gap — the error text is not linked to the input. The only current caller (NewContactDialog) renders dateMaskError's message as a plain <span class="fl-err"> sibling of the row, with no id / aria-describedby pair to the input. A screen reader user hears that the field is invalid but not why, unless they navigate to the error text separately.
  • The ghost template (.fl-mask-ghost) is aria-hidden="true" — it visually echoes the input's own value plus the remaining template, and exposing it too would read the date twice.

Focus

  • The input has no visible border or box of its own — .fl-mask-in is transparent and absolutely positioned over the ghost — so it relies on the surrounding row's focus treatment (formless-dialog's .fl-row:has(input:focus) row tint) rather than drawing a dedicated ring. dialog-formless.css has no separate :focus rule for .fl-mask-in.

Contrast

Computed from packages/ui/src/tokens/tokens.css — the same tokens already measured in dialog/formless-dialog.a11y.md, reused here because the mask sits inside that same dialog body:

  • Typed text (valid) --fg on --surface-100 — 17.59:1 light / 14.53:1 dark.
  • Typed text (invalid) and the error line both use --critical-ink — 5.37:1 / 5.80:1, passes AA.
  • Ghost template .rest --fg-muted — 2.85:1 / 3.63:1, below AA for text; it is a placeholder echo of the template, not content — the same system-wide --fg-muted limit as every other muted placeholder in this family.

Known limits

  • The error message has no aria-describedby link to the input (see ARIA).
  • No calendar complement exists in the shipped code, so there is no alternative input method for someone who cannot type the mask — .claude/rules/design-system.md treats the calendar as a required complement, not an optional one; this field currently has none at all.
  • No live region and no "character rejected" feedback: a dropped keystroke (e.g. a letter) simply does not appear, with nothing announced to a screen reader user.

Source: docs/text-field/masked-date-input.a11y.md · GitLab