- name
- php-laravel-blade-accessibility
- description
- Builds accessible server-rendered PHP/Laravel applications using Blade templates with accessible form patterns, Livewire integration, aria attributes, error announcements with alerts, and tools lens-for-laravel and laravel-blade-ally.
- license
- MIT
- compatibility
- opencode
- metadata
- {"version":"1.0.0","domain":"coding","role":"implementation","scope":"implementation","output-format":"code","content-types":["code","patterns","examples"],"triggers":"PHP, Laravel, Blade, Livewire, accessible forms, aria attributes, lens-for-laravel, laravel-blade-ally","related-skills":"semantic-html-aria-accessibility-tree, automated-a11y-testing-axe-core","archetypes":["tactical","implementation"],"anti_triggers":["React framework","Vue framework","client-side rendering only","backend database"],"response_profile":{"verbosity":"low","directive_strength":"high","abstraction_level":"operational"}}
# Laravel Blade Accessibility: Forms & Livewire
Builds accessible server-rendered PHP/Laravel applications using Blade templates with proper form patterns, ARIA attributes (aria-invalid, aria-describedby), form validation with error announcements (role="alert"), and Livewire reactive patterns. Integrates lens-for-laravel and laravel-blade-ally tools for accessibility audits. Load when building Laravel forms, implementing Livewire components with ARIA, or ensuring server-rendered HTML accessibility.
## TL;DR Checklist
- [ ] Use Blade form components with proper label associations
- [ ] Add `aria-invalid` and `aria-describedby` to form inputs
- [ ] Display validation errors with `role="alert"` for screen reader announcements
- [ ] Use Livewire `wire:loading` with `aria-live="polite"` for loading states
- [ ] Associate labels: `<label for="field-id">`
- [ ] Group related fields with `<fieldset>` and `<legend>`
- [ ] Use `lens-for-laravel` for accessibility analysis
- [ ] Run `php artisan ally:analyze` (laravel-blade-ally) in CI/CD
- [ ] Test forms with jest-axe for blade templates
---
## When to Use
Use this skill when:
- Building accessible forms in Laravel Blade
- Implementing Livewire reactive components with form validation
- Ensuring server-rendered HTML meets WCAG 2.1 AA
- Adding ARIA attributes to form fields
- Setting up accessibility testing for Laravel projects
- Building validation feedback with screen reader support
---
## When NOT to Use
Avoid this skill for:
- React/Vue client-side rendering (use framework-specific skills)
- Database design or backend logic (use data skills)
- Laravel routing or controller logic beyond accessibility
- CSS/styling concerns (use CSS skills)
- Blade templating syntax (use Laravel docs)
---
## Core Workflow
1. **Create Form Component** — Build reusable Blade form components with labels
2. **Add ARIA Attributes** — Use aria-invalid, aria-describedby on inputs
3. **Implement Validation** — Show errors with role="alert" for announcements
4. **Integrate Livewire** — Add wire:loading, wire:target, aria-live
5. **Add Descriptions** — Link help text with aria-describedby
6. **Test with Tools** — Use lens-for-laravel and laravel-blade-ally
7. **Verify with axe** — Run automated testing on Blade output
8. **Screen Reader Test** — Manual test with VoiceOver/NVDA
---
## Implementation Patterns
### Pattern 1: Accessible Form Field Component
```blade
{{-- resources/views/components/form/field.blade.php --}}
@props([
'name' => '',
'label' => '',
'type' => 'text',
'required' => false,
'error' => null,
'description' => null,
'value' => '',
'placeholder' => '',
])
@php
$errorId = $name . '-error';
$descriptionId = $name . '-description';
$hasError = $error !== null;
$describedBy = [];
if ($description) {
$describedBy[] = $descriptionId;
}
if ($hasError) {
$describedBy[] = $errorId;
}
$ariaDescribedBy = count($describedBy) > 0 ? implode(' ', $describedBy) : null;
@endphp
<div class="form-field {{ $hasError ? 'form-field--error' : '' }}">
{{-- Label --}}
<label for="{{ $name }}" class="form-field__label">
{{ $label }}
@if ($required)
<span aria-label="required" class="required-mark"> *</span>
@endif
</label>
{{-- Input --}}
<input
id="{{ $name }}"
name="{{ $name }}"
type="{{ $type }}"
value="{{ old($name, $value) }}"
placeholder="{{ $placeholder }}"
@required($required)
aria-required="{{ $required ? 'true' : 'false' }}"
aria-invalid="{{ $hasError ? 'true' : 'false' }}"
@if ($ariaDescribedBy) aria-describedby="{{ $ariaDescribedBy }}" @endif
class="form-field__input
{{ $hasError ? 'form-field__input--error' : '' }}"
/>
{{-- Description --}}
@if ($description)
<p id="{{ $descriptionId }}" class="form-field__description">
{{ $description }}
</p>
@endif
{{-- Error Message --}}
@if ($hasError)
<p id="{{ $errorId }}" class="form-field__error" role="alert">
{{ $error }}
</p>
@endif
</div>
```
**Usage:**
```blade
<form method="POST" action="/contact">
@csrf
<x-form.field
name="email"
label="Email Address"
type="email"
required
:error="$errors->first('email')"
description="We'll never share your email with anyone else."
/>
<x-form.field
name="message"
label="Message"
type="textarea"
required
:error="$errors->first('message')"
/>
<button type="submit">Send</button>
</form>
```
### Pattern 2: Form with Validation
```blade
{{-- resources/views/contact.blade.php --}}
<form method="POST" action="/contact" novalidate>
@csrf
{{-- Email Field --}}
<div class="form-group">
<label for="email" class="form-label">
Email Address <span aria-label="required">*</span>
</label>
<input
id="email"
name="email"
type="email"
value="{{ old('email') }}"
required
aria-required="true"
aria-invalid="{{ $errors->has('email') ? 'true' : 'false' }}"
aria-describedby="{{ $errors->has('email') ? 'email-error' : null }}"
class="form-input
{{ $errors->has('email') ? 'form-input--error' : '' }}"
/>
@if ($errors->has('email'))
<p id="email-error" class="form-error" role="alert">
{{ $errors->first('email') }}
</p>
@endif
</div>
{{-- Message Field --}}
<div class="form-group">
<label for="message" class="form-label">
Message <span aria-label="required">*</span>
</label>
<textarea
id="message"
name="message"
required
aria-required="true"
aria-invalid="{{ $errors->has('message') ? 'true' : 'false' }}"
aria-describedby="{{ $errors->has('message') ? 'message-error message-hint' : 'message-hint' }}"
class="form-textarea
{{ $errors->has('message') ? 'form-textarea--error' : '' }}"
>{{ old('message') }}</textarea>
<p id="message-hint" class="form-hint">
Maximum 1000 characters
</p>
@if ($errors->has('message'))
<p id="message-error" class="form-error" role="alert">
{{ $errors->first('message') }}
</p>
@endif
</div>
{{-- Checkbox Field --}}
<div class="form-group">
<input
id="terms"
name="terms"
type="checkbox"
required
aria-required="true"
aria-invalid="{{ $errors->has('terms') ? 'true' : 'false' }}"
aria-describedby="{{ $errors->has('terms') ? 'terms-error' : null }}"
/>
<label for="terms" class="form-label">
I agree to the <a href="/terms" target="_blank">terms of service</a>
</label>
@if ($errors->has('terms'))
<p id="terms-error" class="form-error" role="alert">
{{ $errors->first('terms') }}
</p>
@endif
</div>
<button type="submit" class="btn btn-primary">
Submit
</button>
</form>
```
### Pattern 3: Livewire Form Component with ARIA
```php
// app/Livewire/ContactForm.php
namespace App\Livewire;
use Livewire\Component;
class ContactForm extends Component
{
public string $email = '';
public string $message = '';
protected array $rules = [
'email' => 'required|email',
'message' => 'required|min:10|max:1000',
];
protected array $messages = [
'email.required' => 'Please enter your email address.',
'email.email' => 'Please enter a valid email address.',
'message.required' => 'Please enter a message.',
'message.min' => 'Message must be at least 10 characters.',
'message.max' => 'Message must not exceed 1000 characters.',
];
public function submit()
{
$this->validate();
// Send email or save to database
session()->flash('success', 'Your message has been sent!');
$this->reset();
}
public function render()
{
return view('livewire.contact-form');
}
}
```
```blade
{{-- resources/views/livewire/contact-form.blade.php --}}
<form wire:submit="submit" novalidate>
{{-- Status Message --}}
@if (session('success'))
<div class="alert alert-success" role="alert" aria-live="assertive">
{{ session('success') }}
</div>
@endif
{{-- Email Field --}}
<div class="form-group">
<label for="email" class="form-label">
Email Address <span aria-label="required">*</span>
</label>
<input
id="email"
wire:model="email"
type="email"
required
aria-required="true"
aria-invalid="{{ $errors->has('email') ? 'true' : 'false' }}"
aria-describedby="{{ $errors->has('email') ? 'email-error' : null }}"
class="form-input {{ $errors->has('email') ? 'form-input--error' : '' }}"
/>
@error('email')
<p id="email-error" class="form-error" role="alert">
{{ $message }}
</p>
@enderror
</div>
{{-- Message Field --}}
<div class="form-group">
<label for="message" class="form-label">
Message <span aria-label="required">*</span>
</label>
<textarea
id="message"
wire:model="message"
required
aria-required="true"
Ver no GitHub