Input Masking & Conditional Form Logic
Mastering input masks and conditional fields cuts form errors, reduces abandonment, and guides users through complex workflows without cognitive overload.
9 min read
The full lesson
Forms are where products earn or lose trust. When a user fills in a phone number, credit card, or date, they should not have to think about formatting — the interface should shape the value as they type. And showing every possible field up front, regardless of relevance, is both intimidating and slow to fill out.
Two patterns solve these problems. Input masking controls the shape of what users enter. Conditional form logic controls what fields users see based on what they have already answered.
Done well, both patterns feel invisible. Done poorly, they create the exact friction they were designed to remove.
What Input Masking Actually Is
An input mask constrains a text field to a specific character pattern. It formats or filters keystrokes in real time. As a user types 4111, a credit card mask automatically inserts spaces to show 4111 1111 1111 1111. The mask does the formatting work so the user does not have to.
There are three practical forms:
- Format-on-type masks — inserts separators (spaces, dashes, slashes) as the user types. Classic examples: phone numbers and credit cards.
- Character-class masks — restricts each position to an allowed set (digits only, letters only, alphanumeric). The user simply cannot enter an invalid character type.
- Static pattern display — shows a placeholder template (e.g.,
MM/DD/YYYY) inside the field as a ghost overlay, which disappears as each character fills its slot.
When a Mask Helps vs. Hurts
Masks add real value for fields that are:
- Fixed-length — phone numbers, SSNs, postal codes, dates, credit card numbers, IBANs.
- Heavily formatted — values where visual chunking aids reading, like card numbers in groups of four.
- Error-prone without guidance — users frequently enter
01012026when the field expects01/01/2026.
Masks cause problems when applied to:
- Variable-length fields — names, addresses, free-text comments.
- International variants — a US phone mask
(###) ###-####breaks for UK, German, or Brazilian numbers. - Fields where the user pastes — paste often drops characters or misbehaves with rigid masks.
Implementing Masks Accessibly
The most important rule: never confuse the displayed value with the stored value. A phone number displayed as (415) 555-0142 should be stored and submitted as 4155550142 or +14155550142. Strip mask characters in the submit handler or on the server. Sending (415) 555-0142 — with parentheses and spaces — to an API that expects E.164 format causes silent failures.
Accessibility requirements per WCAG 2.2:
- Visible label always present — never use placeholder text as the only label. A format hint like
MM/DD/YYYYbelongs below the field as helper text, not inside it as the label. - aria-describedby for format hints — if you show
Format: (###) ###-####as helper text, wire it:aria-describedby="phone-hint". Screen readers will announce both the label and the hint. - Do not block non-printable keys — masks that intercept keydown must pass through Tab, Backspace, Delete, arrow keys, and modifier combinations. Blocking them breaks keyboard navigation.
- Announce formatting changes — when the mask auto-inserts a separator (like the
/in a date field), a screen reader user heard the digit they typed but not the/. Consider anaria-live="polite"region that reflects the current field value, or use a mask library that handles this natively (IMask and Cleave.js with accessibility patches both do).
WCAG 2.2 also added Accessible Authentication (3.3.8). Authentication flows must not rely solely on cognitive tests like CAPTCHAs — users need an alternative. Input masks that strip or reformat values pasted from a password manager violate the spirit of this rule, because they block the copy-paste mechanism people rely on to authenticate without memorization.
<label for="phone">Mobile phone</label>
<input
id="phone"
type="tel"
inputmode="numeric"
autocomplete="tel"
aria-describedby="phone-hint"
placeholder="(415) 555-0142"
/>
<span id="phone-hint" class="field-hint">US numbers only — 10 digits</span>
A few things to note here. inputmode="numeric" pulls up the numeric keypad on mobile without changing the semantic type to number (which adds a spinner and strips leading zeros). type="tel" is the right semantic choice for phone fields — it also triggers the dial-pad keyboard on iOS and Android.
The Right Mask Library
In 2026, the most defensible choices are:
| Library | Strengths | Weaknesses |
|---|---|---|
| IMask.js | Framework-agnostic, rich pattern DSL, handles dynamic masks | Larger bundle (~14 kB min+gz) |
| react-imask | React wrapper around IMask, hooks API | React-only |
| Cleave.js | Simple API, credit card detection | Accessibility gaps, low maintenance cadence |
| @formkit/auto-animate + custom | Roll-your-own with minimal JS | Requires more implementation work |
For most teams, IMask or react-imask is the right default. Avoid writing your own keydown interceptor — the edge cases (IME input for CJK characters, emoji, browser extensions) are subtle and easy to get wrong.
Conditional Form Logic: Show Only What’s Relevant
A conditional form reveals, hides, or changes fields based on earlier answers. The classic example: selecting “Business” as account type reveals a company name field and a VAT number field. Selecting “Individual” hides both.
This pattern directly fights form abandonment. Research consistently shows that perceived form length is a stronger predictor of abandonment than actual length. Users see a long form and disengage before starting. Conditional logic lets you present a short, relevant form that only grows in response to user choices — keeping perceived length low throughout.
Progressive Disclosure vs. Branching Logic
These are two variants worth distinguishing.
Progressive disclosure reveals additional fields within the same form as a user completes earlier fields or triggers certain answers. The form stays on one page; fields appear inline. This works best for short expansions of one to three additional fields.
Branching logic routes users to entirely different screens or form steps based on an answer. Use this when two paths share very few fields, or when one path is substantially longer than the other. A mortgage pre-qualification form that routes homeowners differently from first-time buyers is a branching scenario.
Do
Use smooth in-place expansion (CSS height transition or layout animation) when revealing new fields. This preserves spatial context and makes it clear that the new fields follow from the user’s previous answer. Make revealed fields focus-manageable: move focus to the first newly-revealed field, or at minimum ensure Tab order flows naturally through newly-added content. Use ARIA to communicate field visibility: set aria-hidden=“true” on hidden field containers, and remove it when they are shown.
Don't
Don’t jump the user to the top of the form when new fields appear — it is disorienting and forces them to re-scan content they already processed. Don’t preserve hidden field values on submission — if a user selects “Individual” (hiding the company name field) and then submits, don’t send a stale company name they entered before switching. Clear hidden field values on hide. Don’t use display:none alone for conditional fields without also removing them from the Tab sequence — keyboard users will Tab into invisible fields they cannot see.
State Management for Conditional Fields
Every conditional field has at least four states: hidden, visible-empty, visible-filled-valid, and visible-filled-invalid. You must design and implement all of them — not just the visible state.
The critical implementation decision is whether to destroy or hide.
- Hide (CSS visibility / aria-hidden) — the field stays in the DOM, holds its value, and participates in validation if you are not careful to exclude it. Faster to show and hide, but risky if you forget to skip hidden fields during validation.
- Destroy (conditional render / v-if / ngIf) — the field is removed from the DOM entirely when hidden. No stale values, no accidental validation, cleaner tab order. Slightly more expensive to mount.
For most product work, destroy is safer. The performance cost of remounting a handful of inputs is negligible. The bugs from stale hidden values submitting to the server are not.
// React example: destroy pattern
function ShippingForm() {
const [isGift, setIsGift] = React.useState(false);
return (
<form>
<label>
<input type="checkbox" onChange={e => setIsGift(e.target.checked)} />
This is a gift
</label>
{isGift && (
<fieldset>
<legend>Gift message</legend>
<label htmlFor="gift-msg">Message (optional)</label>
<textarea id="gift-msg" name="giftMessage" rows={3} />
</fieldset>
)}
</form>
);
}
When isGift becomes false, the fieldset is fully unmounted. No stale giftMessage value survives to the submit payload.
Focus Management
When a conditional section appears, do not leave focus where it is and expect users to Tab to the new content — especially on long forms. The correct pattern:
- The conditional section appears.
- Move programmatic focus to the first interactive element inside the newly revealed section, or to a heading or container with
tabindex="-1"if the section has a header.
function revealSection(sectionRef) {
sectionRef.current.removeAttribute('hidden');
const firstInput = sectionRef.current.querySelector('input, select, textarea');
if (firstInput) firstInput.focus();
}
This step is especially important for keyboard-only users and screen-reader users, who have no other signal that new content appeared.
Validation Integration
Input masking and conditional logic both affect when and how you validate. The modern approach is validate on blur — when a field loses focus — not on every keypress and not only on final submit.
For masked fields, validate the unmasked value. Strip separators before running your regex or length check. A 10-digit phone number validator should receive 4155550142, not (415) 555-0142.
For conditional fields, apply two rules:
- Required validation only applies to visible fields. If a field is hidden, it is never required — regardless of your schema definition.
- Error messages must be specific. “Invalid input” tells users nothing. “Enter a 10-digit US phone number” tells users exactly what to fix. Wire error messages with
aria-describedbyso screen readers associate them with the correct input.
<input
id="phone"
type="tel"
aria-describedby="phone-hint phone-error"
aria-invalid="true"
/>
<span id="phone-hint" class="field-hint">10-digit US number</span>
<span id="phone-error" class="field-error" role="alert">
Enter a 10-digit US phone number, e.g. 4155550142
</span>
role="alert" on the error span triggers an immediate screen-reader announcement when the error appears, without requiring the user to re-focus the field.
Multi-Step Forms and Conditional Routing
When a conditional form grows beyond five to seven fields, consider a multi-step (wizard) pattern. Each step focuses on one concern. Completing a step determines what comes next.
Key rules for multi-step conditional forms:
- Show a progress indicator that reflects the user’s actual path. If Step 3 is irrelevant for a given user, the indicator should show that. Do not show “Step 2 of 5” when the user will only ever see three steps.
- Allow back navigation that restores previously entered values. Never wipe answers when a user goes back — this is a top cause of rage-clicks on multi-step forms.
- Persist draft state for longer flows. If a user abandons a 12-step mortgage application halfway through, they should be able to resume where they left off. Use localStorage for anonymous users and server-side storage for authenticated users.
- Summarize answers at review steps. Before final submission, show a condensed summary of all collected answers with inline edit links. This is the pattern used by the best tax, insurance, and onboarding flows.
Testing Conditional Forms
Unit testing conditional logic is straightforward — test that the correct fields appear for specific input combinations. Integration and end-to-end tests surface the subtler issues:
- Paste and autofill behavior with masks
- Screen reader announcement of newly revealed fields
- Submit payload shape across different conditional paths
- Back-navigation value restoration
- Mobile virtual keyboard behavior with
inputmodeandtypeattributes
Run your masked fields through VoiceOver (macOS/iOS) and NVDA (Windows) at minimum. The experience often diverges significantly from sighted keyboard use in ways automated tests will not catch.