accessible forms

Accessible Forms — Live Demos

5 interactive examples

Three labeling methods — all correct, different use cases

Interact with each field below. The log shows what a screen reader would announce on focus (label + description).

1. <label for>

First and last name
Best: explicit, expands click target, works with all SRs

2. aria-label

Search
No visible label — aria-label provides it
Use when no visible label text exists

3. aria-labelledby

Billing email

address References the heading + hidden text
Use when visible text already describes the control

❌ Common mistakes

Placeholder disappears on focus — SR users lose context
"required" attribute + visible * = SR announces "asterisk required"
--:--:--Tab into any field to see what a screen reader would announce

The legend verbosity problem — and how to fix it

Screen readers repeat the legend text before every radio/checkbox label. Tab through the two groups below and read the SR announcements in the log.

⚠ Long legend (bad UX for screen readers) SR announces: "What is your preferred method of contact for order updates? Phone, radio button, 1 of 3"
Then again: "What is your preferred method of contact for order updates? Email, radio button, 2 of 3"
That's 12 words repeated 3 times = 36 words of repeated context.
❌ Long legend — repeats on every item
What is your preferred method of contact for order updates?
✅ Short legend — minimal repetition
Contact preference

We'll use this for order updates and receipts.

--:--:--Tab through the radio buttons — see what SR announces for each

The 5-step submit error pattern — complete implementation

Submit the form with errors to see all 5 steps fire in order: clear → summary → focus → aria-invalid → inline error. Watch the step badges and log.

1 Clear 2 Summary 3 Focus 4 aria-invalid 5 Inline errors
First and last name
Optional — format: 555-867-5309
--:--:--Click Submit with empty fields to trigger the 5-step error pattern

aria-invalid and aria-errormessage — live state demo

Type in the email field and interact. See how aria-invalid and aria-errormessage state changes in real time. The attribute inspector shows the current values.

We'll send your confirmation here
// Live attribute values
aria-invalid = "false"
aria-errormessage = "ae-error"
aria-describedby = "ae-hint"
// SR announces on focus:
"Email address, required. We'll send your confirmation here."

aria-errormessage vs aria-describedby

aria-describedby

✓ Announces on every focus
✓ Use for hints (always relevant)
✓ Accepts multiple IDs
✓ Universal browser support

aria-errormessage

✓ Only announces when aria-invalid="true"
✓ Semantically correct for errors
✓ One ID only
✓ Chrome 67+, FF 116+, Safari 16.4+
--:--:--Type in the email field and click Validate to see attribute state changes

Password show/hide — the correct pattern

Three things must happen: (1) switch input.type, (2) update aria-label and aria-pressed on the button, (3) keep focus on the INPUT — not the button.

8+ characters, one uppercase, one number
--:--:--Type a password and click the eye icon — focus must stay on the input

Character counter — debounced aria-live announcement

Visual display updates every keystroke. Screen reader announcement is debounced 800ms so it doesn't interrupt typing.

280 characters remaining
--:--:--Type in the textarea — SR announced after 800ms pause (debounced)

autocomplete tokens — WCAG 1.3.5 AA requirement

Every personal data field needs an appropriate autocomplete token. Try filling the first field — autofill should populate related fields.

autocomplete="name"
autocomplete="email"
autocomplete="tel"
autocomplete="street-address"
autocomplete="address-level2"
autocomplete="postal-code"
Read the tutorial