smithery.ai

e11y-fix

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

First seen Mar 30, 2026

Installation

$ npx skills add https://smithery.ai

Similar popular skills

Related neighbors and high-traction skills in the same topics — useful to compare before installing.

Also in this package

Other skills from smithery.ai · top by installs.

npx skills add https://smithery.ai

Browse all from smithery.ai

More details

Agent compatibility

Declared targets from SKILL.md / docs. Unmarked agents are not listed — the skill may still install via the CLI.

Claude Code Not declared
Cursor Not declared
Codex Not declared
GitHub Copilot Not declared
Windsurf Not declared
Gemini CLI Not declared
Cline Not declared
OpenCode Not declared

Package contents

Files included with this skill beyond the listing page.

  • skill md SKILL.md 7,519 B
  • docs SUMMARY.md 131 B

History

  1. First seen on skills.sh
  2. First recorded snapshot · 1 installs

SKILL.md

Excessibility Fix - axe-core Error Reference

Quick reference for fixing axe-core/WCAG errors in Phoenix LiveView applications.

Debugging Workflow

  1. Run mix excessibility to see axe-core 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

axe-core 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"

<!-- 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. axe-core may flag this. You can:

  1. Add a submit button (even if styled invisibly)
  2. Configure axe-core rules to ignore this rule for specific forms
  3. Use aria-label on the form to explain the interaction
<!-- 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

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

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

Error messages

<.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"

<!-- 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"

<!-- 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")

<!-- 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"

<!-- 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"

<!-- 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"

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

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

# 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

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

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

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

LiveView Forms

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

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

axe-core Configuration

axe-core violations include structured data with id, impact, description, helpUrl, and nodes. Each node contains the failing HTML element and fix suggestions.

For legitimate false positives, you can configure axe-core rule disabling in your test setup or via the --disable-rules flag:

mix excessibility --disable-rules=color-contrast,form-field-multiple-labels

Common ignore patterns for LiveView:

  • Forms without visible submit buttons (phx-submit handles it)
  • Dynamic content warnings during transitions

Verification

After fixing:

# Run axe-core 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