Skip to main content

wcag-form-accessibility-nodejs

Implements WCAG 2.2 AA form accessibility patterns for Node.js/JavaScript (server-side rendering, React/Next.js), including label-to-input association, error identification, focus management, dynamic updates with aria-live, and form state management with aria-invalid/aria-required.

Jump to install

Source facts

Repository
paulpas/agent-skill-router
Last source activity
September 23, 2026 at 21:59
Detected SKILL.md language
English
Stars
6
Forks
0

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions ยท Read-only preview
name
wcag-form-accessibility-nodejs
description
Implements WCAG 2.2 AA form accessibility patterns for Node.js/JavaScript (server-side rendering, React/Next.js), including label-to-input association, error identification, focus management, dynamic updates with aria-live, and form state management with aria-invalid/aria-required.
license
MIT
compatibility
opencode
metadata
{"version":"1.0.0","domain":"coding","role":"implementation","scope":"implementation","output-format":"code","triggers":"wcag form accessibility, form validation a11y, aria-invalid, aria-describedby, screen reader forms, keyboard form navigation, next.js server actions forms, how do i make accessible forms","related-skills":[],"archetypes":"tactical","anti_triggers":"brainstorming, vague ideation","response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# WCAG Form Accessibility for Node.js/JavaScript Implements WCAG 2.2 AA compliant form rendering and validation patterns for server-side and client-side JavaScript. When loaded, this skill enables building forms that are fully operable via keyboard, properly announced to screen readers, and resilient to validation errors. Covers label-to-input association (1.3.1), error messaging (3.3.1), focus management (2.4.3), and dynamic content updates (4.1.3). ## TL;DR Checklist - [ ] Every `<input>`, `<select>`, and `<textarea>` has an associated `<label>` with `for="input-id"` attribute matching the input's `id` - [ ] Form submission errors are rendered in a container with `role="alert"` or `aria-live="assertive"` to announce immediately to screen readers - [ ] Each input with validation errors has `aria-invalid="true"` and `aria-describedby="error-id"` linking to the error message element - [ ] Focus is programmatically moved to the first invalid field on form submission failure using `element.focus()` - [ ] Dynamic validation feedback uses `aria-live="polite"` for non-critical messages and `aria-live="assertive"` for errors - [ ] All interactive form controls are reachable via Tab key in logical order (Tab/Shift+Tab) - [ ] Form controls have visible focus indicators (minimum 2px solid ring with 3:1 contrast) - [ ] Server-rendered HTML includes all accessible attributes before JavaScript loads; progressive enhancement is verified - [ ] Tested with screen reader (VoiceOver, NVDA) to confirm labels, errors, and state are announced correctly --- ## When to Use Use this skill when: - Building server-side rendered forms (Express, Fastify, Next.js server components) that must be accessible without client-side JavaScript - Creating React/Next.js form components with real-time validation feedback - Implementing multi-step forms or forms with conditional fields that require dynamic ARIA updates - Handling form submission errors and need to communicate failures to screen reader users - Designing custom form controls (autocomplete, date picker, combobox) with keyboard support - Validating WCAG 2.2 AA compliance for forms in security-sensitive contexts (auth, payments, PII collection) - Migrating legacy forms to meet accessibility requirements or passing accessibility audits --- ## When NOT to Use Avoid this skill for: - Purely presentational non-interactive layouts (use layout patterns instead) - Simple client-side form libraries already providing built-in a11y support (Formik, React Hook Form with auto-labeling) - Rapid prototyping where accessibility is deferred to a later phase (accessibility must be built in from the start, not retrofitted) - Forms where user research has determined your audience does not include keyboard or screen reader users (rare; assume inclusive by default) --- ## Core Workflow 1. **Establish Form Structure and Input Inventory** โ€” List all form inputs, their validation rules, error messages, and conditional visibility. Create an accessibility checklist: Does each input have a unique `id` and associated `<label>`? Are error messages descriptive and linked via `aria-describedby`? **Checkpoint:** Run the form through `axe DevTools` or Lighthouse a11y audit; expect zero "critical" or "serious" violations. 2. **Implement Server-Side Rendering with Semantic HTML** โ€” Use native `<form>`, `<input>`, `<label>`, and `<select>` elements. Generate stable, unique `id` attributes for each input (use a counter, UUID, or hash if dynamic). Render labels with `for="input-id"` matching the input's `id`. Include `aria-required="true"` on required fields and `aria-invalid="true"` on fields with errors. **Checkpoint:** Disable JavaScript and verify the form renders with all labels visible and inputs focusable via Tab. 3. **Render Error Messages with Proper ARIA Linkage** โ€” Create an error container with `id="errors"` and `role="alert"` at the top of the form. For each field error, render a message in a `<div id="field-error-name">` below or next to the input. Link the input to its error with `aria-describedby="field-error-name"`. Use `aria-invalid="true"` on the input to signal validation failure. **Checkpoint:** Open the page in a screen reader (VoiceOver on macOS or NVDA on Windows); navigate to an invalid input and confirm the error message is announced. 4. **Implement Focus Management on Validation Failure** โ€” On form submission, validate all fields server-side and re-render the page with error markup. Use JavaScript (if available) to programmatically focus the first invalid input: `document.querySelector('[aria-invalid="true"]')?.focus()`. For SPA forms (React, Vue), move focus after validation state updates. **Checkpoint:** Submit a form with errors and verify that focus jumps to the first invalid field; screen readers announce both the field label and the error. 5. **Add Dynamic Validation Feedback with aria-live** โ€” For real-time validation (as the user types), wrap feedback messages in a `<div aria-live="polite" aria-atomic="true">` for non-critical messages (e.g., "Password strength: medium") or `aria-live="assertive"` for errors that require immediate attention (e.g., "Email already in use"). Update the div's text content to trigger announcement. **Checkpoint:** Type into a field with real-time validation and confirm the feedback is announced without interrupting typing. 6. **Test with Keyboard Navigation and Screen Readers** โ€” Walk through the form using only Tab, Shift+Tab, Enter, and Arrow keys (no mouse). Verify Tab order follows the visual flow and focus indicators are always visible. Open the form in a screen reader and navigate through each field, confirming labels, required status, error messages, and form purpose are announced clearly. **Checkpoint:** Complete the entire form using only keyboard + screen reader; no mouse required. --- ## Implementation Patterns ### Pattern 1: Server-Rendered Form with WCAG-Compliant Label and Error Linking Demonstrates a complete server-side form component with label association, error messages linked via `aria-describedby`, and `aria-invalid` state. This pattern works without JavaScript. ```javascript /** * Pattern 1: Server-rendered form with WCAG-compliant labels and error linking. * * This pattern shows how to render a form server-side that is fully accessible * to keyboard and screen reader users, even before JavaScript loads. * * Key WCAG criteria addressed: * - 1.3.1 Info and Relationships: Label-to-input association via for/id * - 3.3.1 Error Identification: Error messages linked to inputs via aria-describedby * - 4.1.2 Name, Role, Value: aria-required and aria-invalid on form controls */ // Express server example app.get('/register', (req, res) => { // In a real app, this comes from form submission with validation errors const errors = {}; const formData = {}; // Generate stable IDs for each form field (can use uuid or hash) const fieldIds = { email: 'field-email', password: 'field-password', confirmPassword: 'field-confirm-password', agreeToTerms: 'field-agree-to-terms', }; const html = ` <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1.0"> <title>Register Account</title> <style> body { font-family: system-ui, -apple-system, sans-serif; max-width: 600px; margin: 2rem auto; } .form-group { margin-bottom: 1.5rem; } label { display: block; margin-bottom: 0.5rem; font-weight: 500; } input, textarea, select { width: 100%; padding: 0.5rem; border: 1px solid #ccc; border-radius: 4px; } input:focus, textarea:focus, select:focus { outline: 2px solid #0066cc; outline-offset: 2px; } input[aria-invalid="true"] { border-color: #dc2626; } .error-message { color: #dc2626; font-size: 0.875rem; margin-top: 0.25rem; } [role="alert"] { background: #fee; padding: 1rem; border-radius: 4px; margin-bottom: 1rem; border-left: 4px solid #dc2626; } </style> </head> <body> <h1>Create Account</h1> <!-- Error summary (WCAG 3.3.1) --> ${Object.keys(errors).length > 0 ? ` <div role="alert" aria-live="assertive"> <strong>Please fix the following errors:</strong> <ul> ${Object.entries(errors) .map(([field, msg]) => `<li><a href="#${fieldIds[field]}">${msg}</a></li>`) .join('')} </ul> </div> ` : ''} <form method="POST" action="/register" novalidate> <!-- Email field --> <div class="form-group"> <label for="${fieldIds.email}">Email Address <span aria-label="required">*</span></label> <input type="email" id="${fieldIds.email}" name="email" value="${formData.email || ''}" required aria-required="true" aria-invalid="${errors.email ? 'true' : 'false'}" ${errors.email ? `aria-describedby="error-${fieldIds.email}"` : ''} autocomplete="email" > ${errors.email ? ` <div id="error-${fieldIds.email}" class="error-message" role="status"> ${errors.email} </div> ` : ''} </div> <!-- Password field --> <div class="form-group"> <label for="${fieldIds.password}">Password <span aria-label="required">*</span></label> <input type="password" id="${fieldIds.password}" name="password" required aria-required="true" aria-invalid="${errors.password ? 'true' : 'false'}" ${errors.password ? `aria-describedby="error-${fieldIds.password}"` : ''} autocomplete="new-password" > ${errors.password ? ` <div id="error-${fieldIds.password}" class="error-message" role="status"> ${errors.password} </div> ` : ''} </div> <!-- Confirm Password field --> <div class="form-group"> <label for="${fieldIds.confirmPassword}">Confirm Password <span aria-label="required">*</span></label> <input type="password" id="${fieldIds.confirmPassword}" name="confirmPassword" required aria-required="true" aria-invalid="${errors.confirmPassword ? 'true' : 'false'}" ${errors.confirmPassword ? `aria-describedby="error-${fieldIds.confirmPassword}"` : ''} autocomplete="new-password" > ${errors.confirmPassword ? ` <div id="error-${fieldIds.confirmPassword}" class="error-message" role="status"> ${errors.confirmPassword} </div> ` : ''} </div> <!-- Checkbox field (terms of service) --> <div class="form-group"> <input type="checkbox" id="${fieldIds.agreeToTerms}" name="agreeToTerms" required aria-required="true" aria-invalid="${errors.agreeToTerms ? 'true' : 'false'}" ${errors.agreeToTerms ? `aria-describedby="error-${fieldIds.agreeToTerms}"` : ''} > <label for="${fieldIds.agreeToTerms}" style="display: inline; margin-left: 0.5rem;"> I agree to the <a href="/terms">Terms of Service</a> </label> ${errors.agreeToTerms ? ` <div id="error-${fieldIds.agreeToTerms}" class="error-message"> ${errors.agreeToTerms} </div> ` : ''} </div> <button type="submit">Create Account</button> </form> <script> // Progressive enhancement: Focus first invalid field if errors exist const firstInvalid = document.querySelector('[aria-invalid="true"]'); if (firstInvalid) { firstInvalid.focus(); } </script> </body> </html> `; res.send(html); }); // โœ… GOOD: All labels have for="id", errors link via aria-describedby, required/invalid states explicit // Screen reader announces: "Email Address, required, edit text, invalid" + error message when focused // Tab key navigates through all fields in order; focus visible on each // โŒ BAD: No labels, no error linking, no required/invalid state // <input type="email" name="email" placeholder="Email"> // <div style="color: red;">Email is required</div> // Screen reader cannot associate label with input; error message is disconnected ``` --- ### Pattern 2: React/Next.js Client-Side Form with useId, useActionState, and aria-live Demonstrates a modern React form using the `useId` hook for stable HTML IDs, `useActionState` (Next.js Server Actions) for server-side validation, and `aria-live` regions for dynamic error announcements. ```typescript /** * Pattern 2: React/Next.js form with useId, useActionState, and aria-live. * * Uses: * - useId() for stable HTML id generation (React 18+) * - useActionState() for Next.js Server Actions integration * - aria-live="assertive" for error region with role="alert" * - aria-describedby linking inputs to error messages * - aria-invalid for field validation state * * WCAG criteria: 1.3.1, 3.3.1, 4.1.2, 4.1.3 */ import { useId, useState } from 'react'; import { useActionState } from 'react'; // Server action (runs on server, handles validation) async function validateAndCreateAccount( _prevState: unknown, formData: FormData ) { const email = formData.get('email') as string; const password = formData.get('password') as string; const confirmPassword = formData.get('confirmPassword') as string; const agreeToTerms = formData.get('agreeToTerms') === 'on'; const errors: Record<string, string> = {}; // Validation logic runs server-side if (!email.trim()) { errors.email = 'Email is required'; } else if (!email.includes('@')) { errors.email = 'Enter a valid email address'; } if (!password) { errors.password = 'Password is required'; } else if (password.length < 12) { errors.password = 'Password must be at least 12 characters'; }
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub