git clone https://github.com/Intense-Visions/harness-engineering
T=$(mktemp -d) && git clone --depth=1 https://github.com/Intense-Visions/harness-engineering "$T" && mkdir -p ~/.claude/skills && cp -r "$T/agents/skills/claude-code/a11y-form-patterns" ~/.claude/skills/intense-visions-harness-engineering-a11y-form-patterns && rm -rf "$T"
agents/skills/claude-code/a11y-form-patterns/SKILL.mdAccessible Form Patterns
Build accessible forms with proper labeling, grouped controls, inline validation, and clear error communication
When to Use
- Creating any form — login, registration, checkout, settings
- Adding validation feedback that assistive technology can perceive
- Grouping related form controls (address fields, payment details)
- Implementing multi-step forms or wizards
- Reviewing existing forms for accessibility compliance
Instructions
- Associate every input with a visible
. Use<label>
(React) orhtmlFor
(HTML) to link the label to the input. Clicking the label should focus the input.for
<label htmlFor="email">Email address</label> <input id="email" type="email" name="email" />
Never use placeholder text as a substitute for a label — it disappears when the user starts typing and has poor contrast.
- Use
and<fieldset>
to group related controls. Screen readers announce the legend as context for each control within the group.<legend>
<fieldset> <legend>Shipping Address</legend> <label for="street">Street</label> <input id="street" name="street" /> <label for="city">City</label> <input id="city" name="city" /> </fieldset> <fieldset> <legend>Payment method</legend> <label><input type="radio" name="payment" value="card" /> Credit card</label> <label><input type="radio" name="payment" value="paypal" /> PayPal</label> </fieldset>
- Mark required fields explicitly. Use the
attribute for native validation andrequired
for custom validation. Include a visible indicator (asterisk with explanation).aria-required="true"
<label htmlFor="name"> Full name <span aria-hidden="true">*</span> </label> <input id="name" required aria-required="true" /> <p className="form-note">Fields marked with * are required.</p>
- Provide inline validation errors linked to the input. Use
to mark the field andaria-invalid
oraria-describedby
to point to the error text.aria-errormessage
<label htmlFor="password">Password</label> <input id="password" type="password" aria-invalid={!!errors.password} aria-describedby={errors.password ? 'password-error' : 'password-hint'} /> <p id="password-hint">Must be at least 8 characters.</p> {errors.password && ( <p id="password-error" role="alert" className="error"> {errors.password} </p> )}
-
Use
oraria-live
for dynamic validation messages. When errors appear after form submission or as the user types, screen readers must be notified.role="alert" -
Provide an error summary at the top of the form on submission failure. List all errors with links to the corresponding fields. Move focus to the summary.
{ errors.length > 0 && ( <div role="alert" tabIndex={-1} ref={errorSummaryRef}> <h2>There are {errors.length} errors in this form</h2> <ul> {errors.map((err) => ( <li key={err.field}> <a href={`#${err.field}`}>{err.message}</a> </li> ))} </ul> </div> ); }
- Use
attributes for common fields. This enables browser autofill and password managers, which benefit users with motor and cognitive disabilities.autocomplete
<input name="name" autocomplete="name" /> <input name="email" autocomplete="email" /> <input name="tel" autocomplete="tel" /> <input name="address" autocomplete="street-address" /> <input name="cc-number" autocomplete="cc-number" />
-
Do not disable the submit button while the form is incomplete. Disabled buttons are not focusable and provide no feedback about what is wrong. Instead, allow submission and show validation errors.
-
Support keyboard submission. Forms should submit when the user presses Enter in a text input. Use
with a<form>
— do not rely on JavaScript click handlers alone.<button type="submit"> -
Use
andinputmode
to show the right keyboard on mobile.type
shows an email keyboard,type="email"
shows a number pad.inputmode="numeric"
Details
Label association methods (in order of preference):
— explicit association, most reliable<label for="id">
wrapping the input — implicit association, works in all browsers<label>
— invisible label, use only when no visible label is possiblearia-label
— references another element as the labelaria-labelledby
attribute — last resort, announced inconsistentlytitle
Multi-step forms: Indicate progress with a step indicator using
aria-current="step". Announce step transitions with aria-live. Allow backward navigation without losing data.
Custom select/dropdown: Native
<select> is fully accessible. Custom dropdowns must implement the combobox or listbox pattern with full keyboard support and ARIA attributes. Consider whether the customization is worth the complexity.
Common mistakes:
- Using
instead ofplaceholder
(disappears, poor contrast)<label> - Disabling paste on password fields (breaks password managers)
- Time-limited forms without extension options (WCAG 2.2.1)
- Validation that only uses color (red border without error text)
- CAPTCHAs without accessible alternatives
Source
https://www.w3.org/WAI/tutorials/forms/
Process
- Read the instructions and examples in this document.
- Apply the patterns to your implementation, adapting to your specific context.
- Verify your implementation against the details and edge cases listed above.
Harness Integration
- Type: knowledge — this skill is a reference document, not a procedural workflow.
- No tools or state — consumed as context by other skills and agents.
Success Criteria
- The patterns described in this document are applied correctly in the implementation.
- Edge cases and anti-patterns listed in this document are avoided.