Skills DirectorySkills Directory
SkillsLearnSecurityCategoriesDocsBlogPro
Sign InSubmit Skill
Skills Directory

Security-tested agent skills for Claude, coding agents, and AI workflows.

Directory

  • Browse Skills
  • All Skills A–Z
  • Claude Skills
  • Claude Code Skills
  • Agent Skills
  • Categories
  • Authors
  • Submit a Skill

Learn

  • Learn Hub
  • Install Claude Skills
  • Write SKILL.md
  • Skills vs MCP
  • Directories Compared

Security

  • Security
  • Methodology
  • Secure Claude Skills
  • Security Badges
  • Chrome Extension
  • Skill Manager

Company

  • About
  • Community
  • Blog
  • API Docs
  • Advertise

2026 Skills Directory. All rights reserved.

ProTermsPrivacyRefunds
Back to skills

Php Laravel Blade Accessibility

ASecurity

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.

6 stars
0 votes
0 copies
0 views
Added 9/23/2026
developmenttypescriptphpbashreactvueexpresstestinggitdatabasebackend

Works with

cli

Security Analysis

A100/100

Scanned 9/23/2026

$npx -y skills add paulpas/agent-skill-router --skill php-laravel-blade-accessibility --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Php Laravel Blade Accessibility?

Add the live security badge to your README — it updates automatically with every re-scan.

Security grade badge for Php Laravel Blade Accessibility
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/paulpas-php-laravel-blade-accessibility/badge)](https://www.skillsdirectory.com/skills/paulpas-php-laravel-blade-accessibility)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
Files
SKILL.md
---
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"
            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

```blade
{{-- 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:**
```bash
composer require --dev jsnover/lens-for-laravel
```

**Configuration:**
```php
// 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:**
```bash
composer require --dev symplify/blade-ally
```

**Usage:**
```bash
# 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):**
```yaml
- name: Check Blade Accessibility
  run: php artisan ally:analyze
  continue-on-error: false # Fail pipeline if violations found
```

---

## Testing Blade Forms with axe

```typescript
// 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

```blade
<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

```blade
<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

```blade
<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

```blade
<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

Attribution

paulpaspaulpas
View sourceSee grades on GitHubMore from paulpas →
SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments (0)

No comments yet. Be the first to comment!

SSkills DirectorySkills Directory

Ship a skill? Prove it's safe.

Free 120-pattern security scan, letter grade, and an embeddable README badge.

Submit a skill

Related Skills

Clean Code

Pragmatic coding standards - concise, direct, no over-engineering, no unnecessary comments

304955 votes

Browser Extension Developer

Use this skill when developing or maintaining browser extension code in the `browser/` directory, including Chrome/Firefox/Edge compatibility, content scripts, background scripts, or i18n updates.

286712 votes

Seo Optimizer

SEO optimization with keyword analysis, readability assessment, technical validation, content quality. Use for search rankings, blog posts, content audits, or encountering keyword density, readability scores, meta tags, schema markup errors.

2222 votes

Google Official Seo Guide

Official Google SEO guide covering search optimization, best practices, Search Console, crawling, indexing, and improving website search visibility based on official Google documentation

1862 votes

Writing Plans

Use when you have a spec or requirements for a multi-step task, before touching code

2927051 votes
View all in development →