Skip to main content

wcag-form-accessibility-php-laravel

Implements WCAG 2.2 AA accessible form patterns in PHP/Laravel using semantic HTML, aria-describedby error linking, fieldset/legend grouping, focus management, and server-rendered form practices for guaranteed accessibility compliance.

설치로 이동

소스 정보

저장소
paulpas/agent-skill-router
최근 소스 활동
2026년 9월 23일 21:59
감지된 SKILL.md 언어
영어
스타
6
포크
0

설치 방법

기본적으로 소스를 먼저 확인하는 Prompt가 선택됩니다. 직접 명령으로 전환하거나 로컬 사본을 다운로드할 수도 있습니다.

소스 파일 검토

설치 여부를 결정하기 전에 SKILL.md와 SkillsMP에 표시된 보조 파일을 읽어 보세요.

SKILL.md 표시 중

SKILL.md
소스 지침 · 읽기 전용 미리보기
name
wcag-form-accessibility-php-laravel
description
Implements WCAG 2.2 AA accessible form patterns in PHP/Laravel using semantic HTML, aria-describedby error linking, fieldset/legend grouping, focus management, and server-rendered form practices for guaranteed accessibility compliance.
license
MIT
compatibility
opencode
metadata
{"version":"1.0.0","domain":"coding","role":"implementation","scope":"implementation","output-format":"code","triggers":"WCAG form accessibility, Laravel form validation, aria-describedby, accessible form groups, Blade form patterns, form error handling, focus management forms, Livewire accessibility","related-skills":[],"archetypes":"tactical","anti_triggers":"brainstorming, vague ideation","response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# WCAG 2.2 AA Accessible Form Patterns in PHP/Laravel This skill enables server-rendered form patterns that achieve WCAG 2.2 AA compliance through semantic HTML, proper label association, error message linking, and focus management. Load this skill when implementing or reviewing forms in Laravel, plain PHP, or WordPress plugins. ## When to Use Use this skill when: - Building HTML forms in Laravel Blade or plain PHP templates - Implementing form validation with error messages and accessibility requirements - Creating fieldsets with radio buttons or checkbox groups - Implementing error summary pages with focus restoration - Migrating forms from accessibility violations to WCAG 2.2 AA compliance - Reviewing form markup for WCAG 1.3.1 (Info & Relationships), 3.3.1 (Error Identification), 2.4.3 (Focus Order), 4.1.2 (Name, Role, Value) - Integrating laravel-blade-ally linter into CI/CD pipelines - Enhancing WordPress Contact Form 7 or Gravity Forms with accessibility hooks - Building Livewire components with accessible form interactions ## When NOT to Use Avoid this skill for: - Client-side form libraries that abstract HTML generation (FormKit, Nuxt forms) — use framework-specific accessibility guidance - Complex SPA form builders without server-side rendering — client-side routing breaks focus management - Form components already meeting AAA standards or exceeding project requirements - Simple, single-input forms where WCAG compliance overhead outweighs benefit (consider: any public form should be accessible) --- ## TL;DR Checklist - [ ] Every input has a `<label>` with matching `for` attribute (WCAG 1.3.1) - [ ] Error messages linked via `aria-describedby` on input element (WCAG 3.3.1) - [ ] Radio/checkbox groups wrapped in `<fieldset>` with `<legend>` (WCAG 1.3.1) - [ ] Focus order follows logical tab sequence (WCAG 2.4.3) - [ ] Error summary with focus management on page reload (WCAG 3.3.1, 3.3.7) - [ ] Form keep-alive prevents data loss on validation error (WCAG 3.3.7) - [ ] laravel-blade-ally linter enabled in CI/CD pipeline (gates accessibility) - [ ] Livewire components announce errors via aria-live regions - [ ] WordPress plugin hooks extend accessibility without forking --- ## Core Workflow 1. **Establish Semantic Structure** — Use `<form>`, `<fieldset>`, `<legend>`, and semantic input types (email, tel, date). **Checkpoint:** Validate form element contains all inputs; fieldsets group related inputs. 2. **Link Labels to Inputs** — Every input must have a `<label>` with `for` attribute matching the input `id`. **Checkpoint:** Run `form.elements[name].labels[0].htmlFor` in browser console; should return the input's id. 3. **Associate Error Messages** — Link error messages to inputs via `aria-describedby`. Error message element must have a unique `id` matching the input's `aria-describedby` value. **Checkpoint:** Inspect `input.getAttribute('aria-describedby')` and verify corresponding element exists and contains error text. 4. **Implement Error Summary** — Create error summary page with all validation errors linked to problem inputs. Focus first error element on page load. **Checkpoint:** Tab to each error link; verify it jumps to the problematic input, which receives focus and has error message visible. 5. **Manage Focus State** — On form submission failure, restore focus to first error input or error summary. Preserve form data via Laravel's `old()` helper or Livewire's `wire:model`. **Checkpoint:** Submit invalid form; verify focus moves to error summary or first problematic input. 6. **Test with Screen Readers** — Use NVDA (free), JAWS, or VoiceOver to verify form is announced correctly. Screen reader should announce label, required status, and error message for each input. **Checkpoint:** Load page with screen reader; listen for full form instruction before attempting interaction. 7. **Lint Blade Templates** — Integrate laravel-blade-ally into CI/CD to catch accessibility violations in pull requests. Fails on missing labels, unlinked errors, and invalid ARIA attributes. **Checkpoint:** Run `php artisan blade-ally` before merge; zero warnings. --- ## Implementation Patterns ### Pattern 1: Basic Label-to-Input Association (WCAG 1.3.1) The foundation of form accessibility is connecting labels to inputs. Every input must have a corresponding `<label>` element with a `for` attribute matching the input's `id`. **Blade Template:** ```blade <form action="{{ route('contact.store') }}" method="POST"> @csrf <div class="form-group"> <label for="email">Email Address <span aria-label="required">*</span></label> <input type="email" id="email" name="email" required aria-required="true" value="{{ old('email') }}" @error('email') aria-invalid="true" @enderror /> </div> <button type="submit">Submit</button> </form> ``` **Key Points:** - `id="email"` on input matches `for="email"` on label - `aria-required="true"` announces required status to assistive technology - `aria-invalid="true"` set when validation error exists - `value="{{ old('email') }}"` preserves user input on validation failure (WCAG 3.3.7) - `<span aria-label="required">*</span>` ensures asterisk is announced as "required" **Common Pitfall — Missing for attribute:** ```blade <!-- ❌ BAD — label not linked to input --> <label>Email Address</label> <input type="email" name="email" /> <!-- ✅ GOOD — label linked via for attribute --> <label for="email">Email Address</label> <input type="email" id="email" name="email" /> ``` ### Pattern 2: Error Messages with aria-describedby (WCAG 3.3.1) Error messages must be associated with their input using `aria-describedby`. The error message element needs a unique `id` that the input references. **Blade Template with Laravel Validation Errors:** ```blade <form action="{{ route('register.store') }}" method="POST"> @csrf <div class="form-group"> <label for="password">Password <span aria-label="required">*</span></label> <input type="password" id="password" name="password" required aria-required="true" @error('password') aria-describedby="password-error" aria-invalid="true" @enderror /> @error('password') <div id="password-error" role="alert" class="error-message"> {{ $message }} </div> @enderror <!-- Help text supplementing error message --> <div id="password-help" class="help-text"> Minimum 8 characters, at least one uppercase letter and one number. </div> </div> <button type="submit">Create Account</button> </form> ``` **Key Points:** - `aria-describedby="password-error"` links input to error message element - Error message element has `id="password-error"` matching aria-describedby value - `role="alert"` announces error immediately to screen readers - Help text in separate element prevents duplicate announcements - When there's no error, `aria-describedby` is not set (only set when error exists) **PHP Controller Example:** ```php <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Validation\Rules\Password; class RegisterController extends Controller { public function store(Request $request) { // Guard clause: validate input at boundary $validated = $request->validate([ 'email' => 'required|email|unique:users', 'password' => ['required', Password::min(8) ->mixedCase() ->numbers()], ]); // Data now safely parsed and trusted $user = User::create($validated); return redirect()->route('register.success'); } } ``` **Multi-Error Pattern — Multiple Descriptions:** ```blade <input type="password" id="password" name="password" aria-describedby="password-error password-help" @error('password') aria-invalid="true" @enderror /> @error('password') <div id="password-error" role="alert">{{ $message }}</div> @enderror <div id="password-help" class="help-text"> 8+ chars, uppercase, number required. </div> ``` ### Pattern 3: Fieldset and Legend for Radio/Checkbox Groups (WCAG 1.3.1) Radio buttons and checkboxes must be grouped using `<fieldset>` with a `<legend>` describing the group. Individual inputs within the group still need labels. **Blade Template — Radio Group Example:** ```blade <fieldset class="form-fieldset"> <legend class="form-legend"> How would you like to be contacted? <span aria-label="required">*</span> </legend> <div class="radio-group"> <div class="radio-option"> <input type="radio" id="contact-email" name="contact_method" value="email" required @checked(old('contact_method') === 'email') /> <label for="contact-email">Email</label> </div> <div class="radio-option"> <input type="radio" id="contact-phone" name="contact_method" value="phone" @checked(old('contact_method') === 'phone') /> <label for="contact-phone">Phone</label> </div> <div class="radio-option"> <input type="radio" id="contact-sms" name="contact_method" value="sms" @checked(old('contact_method') === 'sms') /> <label for="contact-sms">SMS</label> </div> </div> @error('contact_method') <div id="contact-method-error" role="alert" class="error-message"> {{ $message }} </div> @enderror </fieldset> ``` **Key Points:** - `<fieldset>` wraps entire group - `<legend>` describes the group purpose - Each radio input has unique `id` and matching label `for` attribute - Error message applies to fieldset, not individual inputs - `@checked()` preserves selection on validation failure **Checkbox Group Pattern:** ```blade <fieldset class="form-fieldset"> <legend class="form-legend"> Select your interests <span aria-label="required">*</span> </legend> <div class="checkbox-group"> @foreach(['web' => 'Web Development', 'mobile' => 'Mobile Apps', 'devops' => 'DevOps'] as $value => $label) <div class="checkbox-option"> <input type="checkbox" id="interest-{{ $value }}" name="interests[]" value="{{ $value }}" @checked(in_array($value, (array)old('interests', []))) /> <label for="interest-{{ $value }}">{{ $label }}</label> </div> @endforeach </div> @error('interests') <div id="interests-error" role="alert" class="error-message"> {{ $message }} </div> @enderror </fieldset> ``` **Common Pitfall — Missing Fieldset:** ```blade <!-- ❌ BAD — no fieldset, screen reader announces three separate inputs --> <label><input type="radio" name="contact" value="email" /> Email</label> <label><input type="radio" name="contact" value="phone" /> Phone</label> <label><input type="radio" name="contact" value="sms" /> SMS</label> <!-- ✅ GOOD — fieldset groups inputs and announces shared context --> <fieldset> <legend>How to contact you?</legend> <label><input type="radio" name="contact" value="email" /> Email</label> <label><input type="radio" name="contact" value="phone" /> Phone</label> <label><input type="radio" name="contact" value="sms" /> SMS</label> </fieldset> ``` ### Pattern 4: Error Summary with Focus Management (WCAG 3.3.1, 3.3.7) On form submission failure, display an error summary at the top of the page with focus restoration to help users correct mistakes quickly. **Blade Template with Error Summary:** ```blade <form action="{{ route('checkout.process') }}" method="POST"> @csrf @if ($errors->any()) <div role="region" aria-labelledby="error-summary-heading" class="error-summary"> <h2 id="error-summary-heading">Please correct the following errors:</h2> <ul> @foreach ($errors->all() as $error) <li> <a href="#{{ $error->get('key') }}" class="error-link"> {{ $error }} </a> </li> @endforeach </ul>
GitHub에서 보기
이 SKILL.md는 매우 커서 SkillsMP가 여기에는 첫 섹션만 미리 보여줍니다. GitHub에서 보기