Files
claude-skills-reference/engineering-team/a11y-audit/references/examples-by-framework.md
Reza Rezvani 1ba7b77e34 refactor(a11y-audit): extract inline content to reference files (41KB → 9.6KB)
SKILL.md was 1,374 lines / 41KB — the largest in the repo, 2.7x above the
500-line Anthropic limit. Split into focused reference files.

Trimmed: 1,374 → 211 lines (9.6KB)

New reference files (6):
- ci-cd-integration.md (GitHub Actions, GitLab CI, Azure DevOps, pre-commit)
- audit-report-template.md (stakeholder report template)
- testing-checklist.md (keyboard, screen reader, visual, forms)
- color-contrast-guide.md (contrast checker, Tailwind palette, sr-only)
- examples-by-framework.md (Vue, Angular, Next.js, Svelte examples)
- wcag-22-new-criteria.md (WCAG 2.2 new success criteria)

Appended to existing:
- framework-a11y-patterns.md (fix patterns catalog added)

Untouched: aria-patterns.md, wcag-quick-ref.md
No content deleted — everything moved to references.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-26 12:20:03 +01:00

8.0 KiB
Raw Blame History

Accessibility Audit Examples by Framework

Example 1: Vue SFC Form Audit

<!-- BEFORE: src/components/LoginForm.vue -->
<template>
  <form @submit="handleLogin">
    <input type="text" placeholder="Email" v-model="email" />
    <input type="password" placeholder="Password" v-model="password" />
    <div v-if="error" style="color: red">{{ error }}</div>
    <div @click="handleLogin">Sign In</div>
  </form>
</template>

Violations detected:

# WCAG Severity Issue
1 1.3.1 Critical Inputs missing associated <label> elements
2 3.3.2 Major Placeholder text used as only label (disappears on input)
3 2.1.1 Critical <div @click> not keyboard accessible
4 4.1.3 Major Error message not announced to screen readers
5 3.3.1 Major Error not programmatically associated with input
<!-- AFTER: src/components/LoginForm.vue -->
<template>
  <form @submit.prevent="handleLogin" aria-label="Sign in to your account">
    <div class="field">
      <label for="login-email">Email</label>
      <input
        id="login-email"
        type="email"
        v-model="email"
        autocomplete="email"
        required
        :aria-describedby="emailError ? 'email-error' : undefined"
        :aria-invalid="!!emailError"
      />
      <span v-if="emailError" id="email-error" role="alert">
        {{ emailError }}
      </span>
    </div>
    <div class="field">
      <label for="login-password">Password</label>
      <input
        id="login-password"
        type="password"
        v-model="password"
        autocomplete="current-password"
        required
        :aria-describedby="passwordError ? 'password-error' : undefined"
        :aria-invalid="!!passwordError"
      />
      <span v-if="passwordError" id="password-error" role="alert">
        {{ passwordError }}
      </span>
    </div>
    <div v-if="error" role="alert" aria-live="assertive" class="form-error">
      {{ error }}
    </div>
    <button type="submit">Sign In</button>
  </form>
</template>

Example 2: Angular Template Audit

<!-- BEFORE: src/app/dashboard/dashboard.component.html -->
<div class="tabs">
  <div *ngFor="let tab of tabs"
       (click)="selectTab(tab)"
       [class.active]="tab.active">
    {{ tab.label }}
  </div>
</div>
<div class="tab-content">
  <div *ngIf="selectedTab">{{ selectedTab.content }}</div>
</div>

Violations detected:

# WCAG Severity Issue
1 4.1.2 Critical Tab widget missing ARIA roles (tablist, tab, tabpanel)
2 2.1.1 Critical Tabs not keyboard navigable (arrow keys, Home, End)
3 2.4.11 Major No visible focus indicator on active tab
<!-- AFTER: src/app/dashboard/dashboard.component.html -->
<div class="tabs" role="tablist" aria-label="Dashboard sections">
  <button
    *ngFor="let tab of tabs; let i = index"
    role="tab"
    [id]="'tab-' + tab.id"
    [attr.aria-selected]="tab.active"
    [attr.aria-controls]="'panel-' + tab.id"
    [attr.tabindex]="tab.active ? 0 : -1"
    (click)="selectTab(tab)"
    (keydown)="handleTabKeydown($event, i)"
    class="tab-button"
    [class.active]="tab.active">
    {{ tab.label }}
  </button>
</div>
<div
  *ngIf="selectedTab"
  role="tabpanel"
  [id]="'panel-' + selectedTab.id"
  [attr.aria-labelledby]="'tab-' + selectedTab.id"
  tabindex="0"
  class="tab-content">
  {{ selectedTab.content }}
</div>

Supporting TypeScript for keyboard navigation:

// dashboard.component.ts
handleTabKeydown(event: KeyboardEvent, index: number): void {
  const tabCount = this.tabs.length;
  let newIndex = index;

  switch (event.key) {
    case 'ArrowRight':
      newIndex = (index + 1) % tabCount;
      break;
    case 'ArrowLeft':
      newIndex = (index - 1 + tabCount) % tabCount;
      break;
    case 'Home':
      newIndex = 0;
      break;
    case 'End':
      newIndex = tabCount - 1;
      break;
    default:
      return;
  }

  event.preventDefault();
  this.selectTab(this.tabs[newIndex]);
  // Move focus to the new tab button
  const tabElement = document.getElementById(`tab-${this.tabs[newIndex].id}`);
  tabElement?.focus();
}

Example 3: Next.js Page-Level Audit

// BEFORE: src/app/page.tsx
export default function Home() {
  return (
    <main>
      <div className="text-4xl font-bold">Welcome to Acme</div>
      <div className="mt-4">
        Build better products with our platform.
      </div>
      <div className="mt-8 bg-blue-600 text-white px-6 py-3 rounded cursor-pointer"
           onClick={() => router.push('/signup')}>
        Get Started
      </div>
    </main>
  );
}

Violations detected:

# WCAG Severity Issue
1 1.3.1 Major Heading uses <div> instead of <h1> -- no semantic structure
2 2.4.2 Major Page missing <title> (Next.js metadata)
3 2.1.1 Critical CTA uses <div onClick> -- not keyboard accessible
4 3.1.1 Minor <html> missing lang attribute (check layout.tsx)
// AFTER: src/app/page.tsx
import type { Metadata } from 'next';
import Link from 'next/link';

export const metadata: Metadata = {
  title: 'Acme - Build Better Products',
  description: 'Build better products with the Acme platform.',
};

export default function Home() {
  return (
    <main>
      <h1 className="text-4xl font-bold">Welcome to Acme</h1>
      <p className="mt-4">
        Build better products with our platform.
      </p>
      <Link
        href="/signup"
        className="mt-8 inline-block bg-blue-600 text-white px-6 py-3 rounded
                   hover:bg-blue-700 focus-visible:outline focus-visible:outline-2
                   focus-visible:outline-offset-2 focus-visible:outline-blue-600"
      >
        Get Started
      </Link>
    </main>
  );
}
// Also fix: src/app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
  return (
    <html lang="en">
      <body>{children}</body>
    </html>
  );
}

Example 4: Svelte Component Audit

<!-- BEFORE: src/lib/components/Accordion.svelte -->
<script>
  let openIndex = -1;
  function toggle(i) { openIndex = openIndex === i ? -1 : i; }
</script>

{#each items as item, i}
  <div class="header" on:click={() => toggle(i)}>
    {item.title}
  </div>
  {#if openIndex === i}
    <div class="content">{item.body}</div>
  {/if}
{/each}

Violations detected:

# WCAG Severity Issue
1 4.1.2 Critical Accordion missing ARIA roles and properties
2 2.1.1 Critical Headers not keyboard accessible
3 2.5.8 Minor Click targets may be smaller than 24x24px (NEW in WCAG 2.2)
<!-- AFTER: src/lib/components/Accordion.svelte -->
<script>
  export let items = [];
  let openIndex = -1;

  function toggle(i) {
    openIndex = openIndex === i ? -1 : i;
  }

  function handleKeydown(event, i) {
    if (event.key === 'Enter' || event.key === ' ') {
      event.preventDefault();
      toggle(i);
    }
  }
</script>

<div class="accordion">
  {#each items as item, i}
    <h3>
      <button
        class="accordion-header"
        aria-expanded={openIndex === i}
        aria-controls="panel-{i}"
        id="header-{i}"
        on:click={() => toggle(i)}
        on:keydown={(e) => handleKeydown(e, i)}
      >
        {item.title}
        <span class="icon" aria-hidden="true">
          {openIndex === i ? '' : '+'}
        </span>
      </button>
    </h3>
    <div
      id="panel-{i}"
      role="region"
      aria-labelledby="header-{i}"
      class="accordion-content"
      class:open={openIndex === i}
      hidden={openIndex !== i}
    >
      {item.body}
    </div>
  {/each}
</div>

<style>
  .accordion-header {
    min-height: 44px; /* WCAG 2.5.8 Target Size */
    width: 100%;
    padding: 12px 16px;
    cursor: pointer;
    text-align: left;
  }
  .accordion-header:focus-visible {
    outline: 2px solid #005fcc;
    outline-offset: 2px;
  }
</style>