facebook / facebook/astryx

[RFC] Input mask support for free-text inputs (phone, ZIP, SSN, card number)

Open
#4,946 4 comments 0 reactions 0 assignees View on GitHub
enhancement
Dominant language
TypeScript
Stars
13.1k
Forks
1.1k
Avg merge
1d 14h
Merged PRs (30d)
669

Description

### Problem Statement

Astryx has no way to constrain free-text entry to a known format while the user types. TextInput accepts anything; the only structured-entry inputs are date/time-shaped (DateInput, DateRangeInput, DateTimeInput, TimeInput, NumberInput).

Many common fields aren't dates or plain numbers but still have a fixed shape: phone numbers, ZIP/postal codes, SSNs, credit card numbers, currency amounts. Today, teams either:
- accept raw text and validate only on blur/submit (user finds out they're wrong too late), or
- hand-roll their own onChange formatting logic per field, per product, with no shared a11y or IME handling.

The gap is real-time input constraint + visual formatting while typing (e.g. auto-inserting the dashes in a phone number as digits are typed, showing placeholder characters for unfilled segments), not just post-hoc validation, which Field's status prop already covers.

### Evidence of Demand

- USWDS ships this as a first-class pattern: Input mask — https://designsystem.digital.gov/components/input-mask/ (phone, SSN/tax ID, currency, ZIP examples with a documented masking approach).

Image

- MUI documents masked/formatted input as a common integration on top of TextField (via a custom inputComponent, e.g. react-imask) — https://mui.com/material-ui/react-text-field/ — indicating it's expected but not native, which is exactly the gap this RFC is about closing for Astryx (native, themed, accessible, rather than "bring your own third-party masking lib").
- Radix and Chakra don't ship a masked-input primitive either; both leave it to userland libraries, but note it recurs enough that it's a maintained integration point in most design systems and internal Meta forms.
- Frequency: any product with a phone, ZIP, SSN, or payment field hits this. It's one of the most common form-field categories after plain text and numeric.

### Why Existing Components Don't Cover This

Tried composing with TextInput: pass a controlled `value` + custom `onChange` that reformats the string on every keystroke (insert dashes for phone, parens for area code, etc.).

Breaks down because:
- Cursor position management is non-trivial — reformatting the string on each keystroke without manually recalculating and re-setting selection start/end causes the caret to jump to the end, which is broken UX for editing/deleting mid-value.
- No shared placeholder-segment convention (e.g. showing `(___) ___-____` structure) — every team invents their own.
- IME and paste handling isn't addressed by a naive onChange reformatter; pasting a full phone number needs the same mask logic as typing, and this is easy to get wrong per-implementation.
- No accessible announcement of the expected format or of auto-inserted characters — screen reader users get no signal that "5" was heard as "(5" with an auto-inserted paren.
- Doesn't compose with Field's status/validation pattern in any standard way — every team wires this up differently, so error states and mask state disagree across products.

This is the same class of problem DateInput already solves for dates (structured entry, segment navigation, auto-advance) — it just doesn't generalize to phone/ZIP/SSN/card shapes.

### Rough Approaches Considered

1. `InputMask` as its own component (mirrors DateInput's relationship to TextInput), with a small set of built-in named masks:



Escape hatch for custom patterns:

2. A `mask` prop directly on TextInput (composition-first, no new component):

Pro: no new export, stays inside "compose, don't rebuild." Con: TextInput's surface area grows for a fairly specialized behavior (segment/caret handling) that most TextInput consumers will never touch — closer to "confuses the 90% to serve the 10%" than option 1.

Leaning toward option 1 given DateInput's precedent as a separate, purpose-built input rather than a TextInput prop, but flagging both since the "existing props vs. new component" question is exactly what the spec protocol is for.

### Accessibility Considerations

- Maps to the same "structured/segmented text input" pattern DateInput already implements — no novel ARIA pattern needed, but it should reuse DateInput's approach rather than invent a new one.
- Expected format must be exposed to assistive tech, not just shown visually (e.g. aria-describedby pointing at "Format: (555) 555-5555" style hint text), so screen reader users know the constraint before typing.
- Auto-inserted literal characters (dashes, parens) should not be individually announced as the user types past them — only the digits/segments the user actually enters — to avoid a noisy, confusing screen reader experience.
- Full keyboard support: typing, backspacing across auto-inserted literals, and pasting a complete value must all land in the right segments without requiring pointer interaction.
- Benefits: screen reader users (clear format expectations), motor-impairment users (fewer corrective keystrokes from mis-formatted entry), and all users on error-prone fields like SSN/card number where format mistakes are costly to catch late.

### Performance Considerations

(not applicable — this is a single-item input, not a list/table/grid)

### Pre-submission Checklist

- [x] I have read the [Contributing guide](https://github.com/facebook/astryx/wiki/Contributing)
- [x] I have read the [API Conventions](https://github.com/facebook/astryx/wiki/API-Conventions)
- [x] I have checked that existing Astryx components cannot compose to solve this
- [x] This is a general-purpose UI pattern (not specific to one product)

Contributor guide

Open the contributing guide

Research direction

Start by reviewing the existing TextInput, DateInput, and Field patterns mentioned in the RFC, then compare the proposed standalone InputMask component with a TextInput mask prop. Define the mask, caret, paste, IME, keyboard, accessibility, and validation behavior before implementation; done means the project has an agreed API and implementation scope.

Written by the indexing model from the issue text.

Assessment

Tech stack
typescript
Domain
accessibility, frontend
Issue type
Feature
Difficulty
5/5
Estimated time
Over a week
Activity status
Active
Clarity
Needs clarification
Newbie friendliness
35/100

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.