Die Oberfläche ist lokalisiert; Skill-Beschreibungen und -Dokumente werden per KI automatisch in Ihre Sprache übersetzt. Code, Befehle, Pfade und Fachbegriffe bleiben im Original, maßgeblich ist das Dokument des Autors.
Skills entdecken
Produktivität · SKILL.md

php-laravel-blade-accessibility

php-laravel-blade-accessibility

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.

Independent GitHub snapshot · Automated classification · Original documentation governs

Skill-Dokumentation

GitHub

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

{{-- 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:

<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

{{-- 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

// 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');
    }
}
{{-- 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"
            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' : '' }}"
        ></textarea>
        <p id="message-hint" class="form-hint">
            Maximum 1000 characters
        </p>
        @error('message')
            <p id="message-error" class="form-error" role="alert">
                {{ $message }}
            </p>
        @enderror
    </div>

    {{-- Submit with Loading State --}}
    <button
        type="submit"
        class="btn btn-primary"
        wire:loading.attr="disabled"
        aria-busy="false"
        wire:loading="true"
        aria-busy="true"
    >
        <span wire:loading.remove>Send Message</span>
        <span wire:loading aria-live="polite">Sending...</span>
    </button>
</form>

Pattern 4: Select/Dropdown Component

{{-- resources/views/components/form/select.blade.php --}}
@props([
    'name' => '',
    'label' => '',
    'options' => [],
    'selected' => null,
    'required' => false,
    'error' => null,
    'description' => null,
])

@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 for="{{ $name }}" class="form-label">
        {{ $label }}
        @if ($required)
            <span aria-label="required">*</span>
        @endif
    </label>

    <select
        id="{{ $name }}"
        name="{{ $name }}"
        @required($required)
        aria-required="{{ $required ? 'true' : 'false' }}"
        aria-invalid="{{ $hasError ? 'true' : 'false' }}"
        @if ($ariaDescribedBy) aria-describedby="{{ $ariaDescribedBy }}" @endif
        class="form-select {{ $hasError ? 'form-select--error' : '' }}"
    >
        <option value="">Select {{ strtolower($label) }}...</option>
        @foreach ($options as $value => $label)
            <option value="{{ $value }}" @selected(old($name) == $value || $selected == $value)>
                {{ $label }}
            </option>
        @endforeach
    </select>

    @if ($description)
        <p id="{{ $descriptionId }}" class="form-hint">
            {{ $description }}
        </p>
    @endif

    @if ($hasError)
        <p id="{{ $errorId }}" class="form-error" role="alert">
            {{ $error }}
        </p>
    @endif
</div>

Accessibility Tools for Laravel

lens-for-laravel

Provides IDE/editor integration for accessibility issues in Blade templates.

Installation:

composer require --dev jsnover/lens-for-laravel

Configuration:

// config/lens.php
return [
    'enabled' => true,
    'rules' => [
        'missing-alt-text' => true,
        'missing-label' => true,
        'invalid-aria' => true,
        'color-contrast' => false, // Requires CSS parsing
    ],
];

laravel-blade-ally

CLI tool for accessibility analysis of Blade templates.

Installation:

composer require --dev symplify/blade-ally

Usage:

# Analyze all Blade templates
php artisan ally:analyze

# Output accessibility violations
# Example:
# resources/views/contact.blade.php:12
#   Missing <label> for input id="email"
#   Error: Input must have associated label

In CI/CD (GitHub Actions):

- name: Check Blade Accessibility
  run: php artisan ally:analyze
  continue-on-error: false # Fail pipeline if violations found

Testing Blade Forms with axe

// tests/accessibility/ContactFormTest.php
namespace Tests\Accessibility;

use PHPUnit\Framework\TestCase;
use Tests\TestCase as LaravelTestCase;

class ContactFormTest extends LaravelTestCase
{
    public function test_contact_form_has_no_accessibility_violations()
    {
        $response = $this->get('/contact');

        // Parse HTML and test with axe-core-php or jest-axe
        $html = $response->getContent();

        // Alternatively, test with browser testing
        $this->browse(function (Browser $browser) {
            $browser->visit('/contact')
                ->screenshot('contact-form')
                // Add accessibility checks if using browser testing library
                ->press('Send Message')
                ->waitFor('.form-error')
                ->assertSee('Please enter your email address.');
        });
    }

    public function test_form_displays_validation_errors_accessibly()
    {
        $response = $this->post('/contact', [
            'email' => 'invalid',
            'message' => '',
        ]);

        $response->assertSessionHasErrors(['email', 'message']);
        
        // Verify error messages rendered with role="alert"
        $html = $response->getContent();
        $this->assertStringContainsString('role="alert"', $html);
        $this->assertStringContainsString('aria-invalid="true"', $html);
    }
}

Form Field Component Best Practices

Fieldset for Grouped Fields

<fieldset>
    <legend>Contact Information</legend>

    <x-form.field
        name="first_name"
        label="First Name"
        required
    />

    <x-form.field
        name="last_name"
        label="Last Name"
        required
    />
</fieldset>

<fieldset>
    <legend>Preferences</legend>

    <div class="form-group">
        <input
            id="newsletter"
            name="newsletter"
            type="checkbox"
            value="1"
        />
        <label for="newsletter">
            Subscribe to our newsletter
        </label>
    </div>

    <div class="form-group">
        <input
            id="notifications"
            name="notifications"
            type="checkbox"
            value="1"
        />
        <label for="notifications">
            Enable email notifications
        </label>
    </div>
</fieldset>

Radio Button Group

<fieldset>
    <legend>Shipping Method</legend>

    <div class="form-group">
        <input
            id="standard"
            name="shipping"
            type="radio"
            value="standard"
            required
            aria-required="true"
        />
        <label for="standard">
            Standard (5-7 business days)
        </label>
    </div>

    <div class="form-group">
        <input
            id="express"
            name="shipping"
            type="radio"
            value="express"
            required
            aria-required="true"
        />
        <label for="express">
            Express (2-3 business days)
        </label>
    </div>

    <div class="form-group">
        <input
            id="overnight"
            name="shipping"
            type="radio"
            value="overnight"
            required
            aria-required="true"
        />
        <label for="overnight">
            Overnight (next business day)
        </label>
    </div>
</fieldset>

Livewire Accessibility Patterns

Livewire with aria-live for Dynamic Updates

<div wire:ignore.self class="search-results" aria-live="polite" aria-label="Search results">
    @if ($query)
        <div class="results-count">
            Found {{ $results->count() }} results
        </div>
        @forelse ($results as $result)
            <div class="result-item">
                <a href="{{ $result->url }}">{{ $result->title }}</a>
                <p>{{ $result->description }}</p>
            </div>
        @empty
            <p role="status">No results found for "{{ $query }}"</p>
        @endforelse
    @endif
</div>

<script>
    // Announce updates to screen readers
    document.addEventListener('livewire:updated', () => {
        const results = document.querySelector('[aria-live]');
        if (results) {
            // aria-live="polite" announces changes
        }
    });
</script>

Livewire Form Submission Feedback

<form wire:submit="submit">
    @if ($errors->any())
        <div class="alert alert-error" role="alert" aria-live="assertive">
            <p class="font-bold">Please correct the following errors:</p>
            <ul>
                @foreach ($errors->all() as $error)
                    <li>{{ $error }}</li>
                @endforeach
            </ul>
        </div>
    @endif

    <x-form.field
        name="email"
        label="Email"
        wire:model="email"
        :error="$errors->first('email')"
    />

    <button type="submit" wire:loading.attr="disabled">
        Submit
    </button>

    <div wire:loading aria-live="polite" aria-busy="true">
        Processing your request...
    </div>
</form>

Constraints

MUST DO

  • Associate all labels with inputs using for and id attributes
  • Add aria-invalid and aria-describedby to form inputs
  • Display validation errors with role="alert"
  • Group related fields with <fieldset> and <legend>
  • Use aria-required on required fields
  • Test Blade templates with lens-for-laravel
  • Run php artisan ally:analyze in CI/CD
  • Include aria-live updates in Livewire components
  • Test forms with jest-axe or accessible browser testing

MUST NOT DO

  • Leave inputs without labels
  • Use placeholder text as label (violates WCAG)
  • Display validation errors without role="alert"
  • Remove focus indicators from form fields
  • Create unlabeled fieldsets (use legend)
  • Implement custom form elements without ARIA
  • Skip accessibility testing in CI/CD
  • Ignore laravel-blade-ally violations

Testing Checklist

Form Structure:

  • All inputs have labels (not placeholder-only)
  • Required fields marked with aria-required
  • Error messages have role="alert"
  • Grouped fields use fieldset/legend
  • Help text linked with aria-describedby

Livewire:

  • Validation errors announce immediately
  • Loading states have aria-busy
  • Dynamic content updates have aria-live
  • Focus managed correctly on update
  • Keyboard navigation works post-update

Testing:

  • laravel-blade-ally passes
  • jest-axe tests pass
  • Form submits correctly
  • Error messages display and announce
  • Screen reader announces all fields/errors
Gespeicherte Skills