Skip to content
Back to skills

E11y Fix

ASecurity

Reference guide for fixing Pa11y/WCAG errors in Phoenix LiveView - maps common violations to Phoenix-specific fixes

  • 42 stars
  • 0 votes
  • 0 copies
  • 1 view
  • Added February 7, 2026
code-qualitygobashdebugginggit

Works with

  • cli

Security analysis

A93/100
  • highPerforms destructive filesystem operations

Pro shows the line behind each finding and how to fix it

Scanned February 12, 2026

npx -y skills add lessthanseventy/excessibility --skill e11y-fix --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of E11y Fix?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for E11y Fix
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/lessthanseventy-e11y-fix/badge)](https://www.skillsdirectory.com/skills/lessthanseventy-e11y-fix)

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

Download with Pro
SKILL.md
---
name: e11y-fix
description: Reference guide for fixing Pa11y/WCAG errors in Phoenix LiveView - maps common violations to Phoenix-specific fixes
---

# Excessibility Fix - Pa11y Error Reference

Quick reference for fixing Pa11y/WCAG errors in Phoenix LiveView applications.

## Debugging Workflow

1. Run `mix excessibility` to see Pa11y errors
2. Use `html_snapshot(view)` to capture the problematic state
3. Read snapshot file to see exact HTML
4. Apply fix from reference below
5. Re-run `mix excessibility` to verify

## Quick Reference

| Pa11y Error | WCAG Rule | Phoenix Fix |
|-------------|-----------|-------------|
| Form input without label | 1.3.1 | Add `<.label>` or `aria-label` |
| Missing alt text | 1.1.1 | Add `alt` attribute to `<img>` |
| Low contrast | 1.4.3 | Adjust Tailwind colors |
| Missing lang | 3.1.1 | Add `lang="en"` to `<html>` |
| Empty link | 2.4.4 | Add link text or `aria-label` |
| Missing heading structure | 1.3.1 | Use proper `<h1>`-`<h6>` hierarchy |
| No skip link | 2.4.1 | Add "Skip to content" link |

## Common Fixes by Category

### Forms

#### Input without label

**Error:** "Form element does not have a label"

```heex
<!-- Bad -->
<input type="text" name="email" />

<!-- Good: Using Phoenix form helpers -->
<.input field={@form[:email]} type="email" label="Email address" />

<!-- Good: Manual label -->
<label for="email">Email address</label>
<input type="text" name="email" id="email" />

<!-- Good: aria-label for icon-only -->
<input type="search" aria-label="Search" />
```

#### Form without submit button

**Note:** LiveView forms using `phx-submit` often don't need a visible submit button. Pa11y may flag this. You can:

1. Add a submit button (even if styled invisibly)
2. Configure pa11y.json to ignore this rule for specific forms
3. Use `aria-label` on the form to explain the interaction

```heex
<!-- phx-submit form with hidden submit -->
<form phx-submit="save">
  <.input field={@form[:name]} type="text" label="Name" />
  <button type="submit" class="sr-only">Submit</button>
</form>
```

#### Required fields

```heex
<.input
  field={@form[:email]}
  type="email"
  label="Email"
  required
/>

<!-- Renders with aria-required="true" -->
```

#### Error messages

```heex
<.input
  field={@form[:email]}
  type="email"
  label="Email"
  errors={@form[:email].errors}
/>

<!-- Errors should be associated via aria-describedby -->
```

### Images

#### Missing alt text

**Error:** "Image missing alt attribute"

```heex
<!-- Bad -->
<img src={@avatar_url} />

<!-- Good: Meaningful alt -->
<img src={@avatar_url} alt={"Profile picture of #{@user.name}"} />

<!-- Good: Decorative image (empty alt) -->
<img src="/images/decorative-line.png" alt="" />

<!-- Good: Icon with adjacent text -->
<span>
  <img src="/icons/star.svg" alt="" />
  Favorites
</span>
```

### Links

#### Empty link

**Error:** "Link text is empty"

```heex
<!-- Bad: Icon-only link -->
<.link navigate={~p"/settings"}>
  <.icon name="hero-cog-6-tooth" />
</.link>

<!-- Good: Add aria-label -->
<.link navigate={~p"/settings"} aria-label="Settings">
  <.icon name="hero-cog-6-tooth" />
</.link>

<!-- Good: Add screen reader text -->
<.link navigate={~p"/settings"}>
  <.icon name="hero-cog-6-tooth" />
  <span class="sr-only">Settings</span>
</.link>
```

#### Vague link text

**Error:** "Link text is vague" (e.g., "click here", "read more")

```heex
<!-- Bad -->
<.link navigate={~p"/article/#{@article.id}"}>Read more</.link>

<!-- Good: Descriptive text -->
<.link navigate={~p"/article/#{@article.id}"}>
  Read more about <%= @article.title %>
</.link>

<!-- Good: Use aria-label for context -->
<.link navigate={~p"/article/#{@article.id}"} aria-label={"Read more about #{@article.title}"}>
  Read more
</.link>
```

### Headings

#### Skipped heading level

**Error:** "Heading levels should only increase by one"

```heex
<!-- Bad: h1 then h3 -->
<h1>Page Title</h1>
<h3>Section</h3>

<!-- Good: Sequential levels -->
<h1>Page Title</h1>
<h2>Section</h2>
<h3>Subsection</h3>
```

#### Multiple h1s

**Error:** "Page should have exactly one h1"

```heex
<!-- Ensure only one h1 per page -->
<h1><%= @page_title %></h1>

<!-- Use h2 for section headings -->
<section>
  <h2>Features</h2>
</section>
```

### Color Contrast

#### Low contrast text

**Error:** "Color contrast is too low"

```heex
<!-- Check your Tailwind colors -->
<!-- Bad: gray-400 on white (3.5:1 ratio) -->
<p class="text-gray-400">Low contrast text</p>

<!-- Good: gray-600 on white (5.7:1 ratio) -->
<p class="text-gray-600">Better contrast text</p>

<!-- Good: gray-700 on white (8.6:1 ratio) -->
<p class="text-gray-700">High contrast text</p>
```

Common Tailwind fixes:
- `text-gray-400` → `text-gray-600` or darker
- `text-blue-400` → `text-blue-600` or darker
- For dark backgrounds, use `-100` or `-200` variants

### Modals/Dialogs

#### Modal accessibility

```heex
<div
  id="modal"
  role="dialog"
  aria-modal="true"
  aria-labelledby="modal-title"
>
  <h2 id="modal-title">Confirm Action</h2>
  <p>Are you sure?</p>
  <button phx-click="confirm">Yes</button>
  <button phx-click="cancel">No</button>
</div>
```

**Focus management:** Use `phx-mounted` to trap focus:

```elixir
# In your LiveView
def handle_event("open_modal", _, socket) do
  {:noreply,
   socket
   |> assign(:show_modal, true)
   |> push_event("focus-modal", %{})}
end
```

### Dynamic Content

#### Live regions for updates

```heex
<!-- Announce changes to screen readers -->
<div aria-live="polite" aria-atomic="true">
  <%= if @flash[:info] do %>
    <%= @flash[:info] %>
  <% end %>
</div>

<!-- For important alerts -->
<div role="alert">
  <%= @error_message %>
</div>
```

#### Loading states

```heex
<div aria-busy={@loading}>
  <%= if @loading do %>
    <span class="sr-only">Loading...</span>
    <.spinner />
  <% else %>
    <%= @content %>
  <% end %>
</div>
```

## Phoenix-Specific Notes

### Core Components

Phoenix 1.7+ includes accessible core components. Use them:

```heex
<.input />     <!-- Handles labels, errors, aria -->
<.button />    <!-- Proper button semantics -->
<.modal />     <!-- Focus management, aria -->
<.table />     <!-- Proper table structure -->
```

### LiveView Forms

```heex
<.simple_form for={@form} phx-submit="save">
  <.input field={@form[:email]} type="email" label="Email" />
  <.input field={@form[:password]} type="password" label="Password" />
  <:actions>
    <.button>Save</.button>
  </:actions>
</.simple_form>
```

### Flash Messages

Default Phoenix flashes are accessible. Don't break them:

```heex
<!-- Use the built-in flash component -->
<.flash_group flash={@flash} />
```

## pa11y.json Configuration

For legitimate false positives, configure `pa11y.json`:

```json
{
  "defaults": {
    "standard": "WCAG2AA",
    "runners": ["htmlcs"],
    "ignore": [
      "WCAG2AA.Principle1.Guideline1_3.1_3_1.H44.NotFormControl"
    ]
  }
}
```

Common ignore patterns for LiveView:
- Forms without visible submit buttons (phx-submit handles it)
- Dynamic content warnings during transitions

## Verification

After fixing:

```bash
# Run Pa11y on all snapshots
mix excessibility

# Check specific test
mix excessibility test/my_test.exs

# Verify with fresh snapshots
rm -rf test/excessibility/html_snapshots/*
mix test test/my_test.exs
mix excessibility
```

Attribution

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

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.