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.

Aller à l'installation

Informations de source

Dépôt
paulpas/agent-skill-router
Dernière activité de la source
23 septembre 2026 à 21:59
Langue détectée de SKILL.md
anglais
Étoiles
6
Forks
0

Options d'installation

Le prompt qui vérifie d'abord la source est sélectionné par défaut. Vous pouvez passer à une commande directe ou télécharger une copie locale.

Vérifiez les fichiers source

Lisez SKILL.md et les fichiers associés affichés par SkillsMP avant de décider de l'installer.

Affichage de SKILL.md

SKILL.md
Instructions source · Aperçu en lecture seule
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'; }
Voir sur GitHub
Ce SKILL.md est tres volumineux, SkillsMP affiche donc ici seulement la premiere section. Voir sur GitHub