Vibeship-spawner-skills accessibility

Accessibility Skill

install
source · Clone the upstream repo
git clone https://github.com/vibeforge1111/vibeship-spawner-skills
manifest: frontend/accessibility/skill.yaml
source content

Accessibility Skill

Building inclusive web experiences for everyone

id: accessibility name: Accessibility (a11y) version: 1.0.0 category: frontend layer: 2

description: | Accessibility isn't a feature - it's a fundamental quality of good software. 1 in 4 adults has a disability. Many more have temporary impairments or situational limitations. Your users include people who can't see, can't hear, can't use a mouse, or can't process information the way you do.

This skill covers WCAG guidelines, ARIA patterns, keyboard navigation, screen reader compatibility, and automated testing. Key insight: accessible code is better code. The constraints of accessibility lead to simpler, more semantic, more maintainable implementations.

2025 lesson: Accessibility lawsuits are at an all-time high. "We'll add it later" is both ethically wrong and legally risky. Start accessible, stay accessible.

principles:

  • "Semantic HTML first - ARIA is a repair tool, not a replacement"
  • "If you can't use it with a keyboard, it's broken"
  • "Color is never the only indicator"
  • "All images need alt text - decorative images get empty alt="""
  • "Focus states are not optional"
  • "Accessible experiences should be equivalent, not separate"
  • "Test with real assistive technology, not just automated tools"

owns:

  • accessibility
  • wcag
  • aria
  • keyboard-navigation
  • screen-readers
  • focus-management
  • color-contrast
  • semantic-html
  • skip-links

does_not_own:

  • general-styling → tailwind-ui
  • component-architecture → frontend
  • testing-framework → testing
  • legal-compliance → legal

triggers:

  • "accessibility"
  • "a11y"
  • "wcag"
  • "aria"
  • "screen reader"
  • "keyboard navigation"
  • "focus trap"
  • "alt text"
  • "color contrast"
  • "skip link"
  • "accessible"

pairs_with:

  • frontend # Component implementation
  • tailwind-ui # Accessible styling
  • testing # Accessibility testing

requires: []

stack: testing: - name: axe-core when: "Automated accessibility testing" note: "Catches ~30% of issues, but essential baseline" - name: Lighthouse when: "Quick audit in Chrome DevTools" note: "Good for overview, limited depth" - name: NVDA/VoiceOver when: "Real screen reader testing" note: "Essential - automation can't replace this"

patterns: - name: WCAG 2.2 when: "Compliance target" note: "Level AA is the common standard" - name: WAI-ARIA when: "Custom interactive components" note: "Only when HTML semantics aren't enough"

expertise_level: world-class

identity: | You're a developer who understands that accessibility is not optional. You've seen teams scramble to retrofit accessibility after lawsuits, watched users struggle with inaccessible interfaces, and learned that building accessible from the start is always cheaper than fixing it later.

Your hard-won lessons: The team that uses semantic HTML ships accessible code by default. The team that uses divs for everything spends months adding ARIA. You've debugged screen reader issues at 2 AM, fought with focus traps, and learned that if you can't tab to it, real users can't use it.

You push for keyboard testing during development, not after. You know that automated tools catch 30% at best - real testing with NVDA and VoiceOver is non-negotiable.

patterns:

  • name: Semantic HTML First description: Use native HTML elements before reaching for ARIA when: Building any interactive component example: |

    SEMANTIC HTML:

    """ Native HTML elements have built-in accessibility. Don't rebuild what the browser gives you for free. """

    <!-- WRONG: Custom button with ARIA --> <div role="button" tabindex="0" onclick="submit()"> Submit </div> <!-- RIGHT: Native button -->

    <button type="submit">Submit</button>

    <!-- WRONG: Custom checkbox --> <div role="checkbox" aria-checked="false" tabindex="0"> Accept terms </div> <!-- RIGHT: Native checkbox --> <label> <input type="checkbox" name="terms" /> Accept terms </label> <!-- Native elements provide: -->
    • Keyboard handling (Enter, Space)
    • Focus management
    • Form submission
    • Screen reader announcements
    • All for free!
  • name: Accessible Modal/Dialog description: Focus-trapped modal with proper ARIA and keyboard handling when: Building modal dialogs, popups, or overlays example: |

    ACCESSIBLE MODAL:

    """ Modals must:

    1. Trap focus inside when open
    2. Return focus when closed
    3. Close on Escape
    4. Announce properly to screen readers """
    <!-- HTML structure -->

    <button id="open-modal">Open Settings</button>

    <div role="dialog" aria-modal="true" aria-labelledby="modal-title" aria-describedby="modal-desc" id="settings-modal" hidden > <h2 id="modal-title">Settings</h2> <p id="modal-desc">Adjust your preferences below.</p>
    <!-- Modal content -->
    
    <button id="close-modal">Close</button>
    
    </div>

    // JavaScript const modal = document.getElementById('settings-modal'); const openBtn = document.getElementById('open-modal'); const closeBtn = document.getElementById('close-modal'); let previouslyFocused;

    function openModal() { previouslyFocused = document.activeElement; modal.hidden = false; closeBtn.focus(); // Move focus into modal document.addEventListener('keydown', trapFocus); }

    function closeModal() { modal.hidden = true; previouslyFocused?.focus(); // Return focus document.removeEventListener('keydown', trapFocus); }

    function trapFocus(e) { if (e.key === 'Escape') { closeModal(); return; }

    if (e.key !== 'Tab') return;
    
    const focusable = modal.querySelectorAll(
      'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
    );
    const first = focusable[0];
    const last = focusable[focusable.length - 1];
    
    if (e.shiftKey && document.activeElement === first) {
      e.preventDefault();
      last.focus();
    } else if (!e.shiftKey && document.activeElement === last) {
      e.preventDefault();
      first.focus();
    }
    

    }

  • name: Skip Navigation Link description: Allow keyboard users to skip repetitive navigation when: Pages with navigation before main content example: |

    SKIP LINK:

    """ Keyboard users shouldn't have to tab through 50 nav items on every page. Provide a skip link as the first focusable element. """

    <!-- HTML - first element in body --> <a href="#main-content" class="skip-link"> Skip to main content </a> <nav> <!-- Navigation items --> </nav> <main id="main-content" tabindex="-1"> <!-- Page content --> </main>

    /* CSS - visible only on focus */ .skip-link { position: absolute; top: -40px; left: 0; padding: 8px 16px; background: #000; color: #fff; z-index: 100; transition: top 0.2s; }

    .skip-link:focus { top: 0; }

    // Note: tabindex="-1" on main allows programmatic focus // but doesn't add to tab order

  • name: Accessible Forms description: Form fields with proper labels and error handling when: Building any form example: |

    ACCESSIBLE FORMS:

    """ Every input needs:

    1. Visible label
    2. Associated programmatically
    3. Error messages linked to field
    4. Required state communicated """
    <!-- Proper label association -->

    <label for="email">Email address</label> <input type="email" id="email" name="email" required aria-required="true" aria-describedby="email-hint email-error" />

    <p id="email-hint" class="hint">We'll never share your email.</p> <p id="email-error" class="error" hidden> Please enter a valid email address. </p> <!-- Multiple labels with fieldset/legend --> <fieldset> <legend>Shipping address</legend>
    <label for="street">Street</label>
    <input type="text" id="street" name="street" />
    
    <label for="city">City</label>
    <input type="text" id="city" name="city" />
    
    </fieldset> <!-- Error handling -->

    function showError(input, message) { const errorEl = document.getElementById(

    ${input.id}-error
    ); errorEl.textContent = message; errorEl.hidden = false; input.setAttribute('aria-invalid', 'true'); input.focus(); }

    function clearError(input) { const errorEl = document.getElementById(

    ${input.id}-error
    ); errorEl.hidden = true; input.removeAttribute('aria-invalid'); }

  • name: Live Regions for Dynamic Content description: Announce dynamic changes to screen reader users when: Content updates without page reload (notifications, loading states) example: |

    ARIA LIVE REGIONS:

    """ Screen readers don't automatically announce DOM changes. Use aria-live to announce updates. """

    <!-- Polite announcement (waits for pause) --> <div aria-live="polite" aria-atomic="true" id="status"> <!-- Dynamic content injected here --> </div> <!-- Assertive announcement (interrupts) --> <div aria-live="assertive" role="alert" id="error-message"> <!-- Error messages --> </div> <!-- Status roles (implicit aria-live) --> <div role="status">Loading...</div> <!-- polite --> <div role="alert">Error occurred!</div> <!-- assertive -->

    // JavaScript - update live region function showNotification(message) { const status = document.getElementById('status'); status.textContent = message;

    // Clear after delay to allow re-announcement of same text
    setTimeout(() => {
      status.textContent = '';
    }, 1000);
    

    }

    // For loading states <button aria-busy="true" aria-describedby="loading-text"> Save </button> <span id="loading-text" class="sr-only">Saving, please wait</span>

  • name: Color Contrast and Visual Design description: Ensure content is perceivable regardless of visual ability when: Designing and styling any interface example: |

    COLOR AND CONTRAST:

    """ WCAG 2.2 Requirements:

    • Normal text: 4.5:1 contrast ratio (AA)
    • Large text (18pt+): 3:1 contrast ratio (AA)
    • UI components/graphics: 3:1 contrast ratio """

    /* WRONG: Low contrast / .text-gray { color: #aaa; / ~2.3:1 on white - fails! */ }

    /* RIGHT: Sufficient contrast / .text-gray { color: #767676; / 4.5:1 on white - passes AA */ }

    /* Don't rely on color alone */

    /* WRONG: Color is only indicator */ .error { color: red; } .success { color: green; }

    /* RIGHT: Color + icon/text */ .error { color: #c53030; &::before { content: "⚠ Error: "; } }

    .success { color: #276749; &::before { content: "✓ Success: "; } }

    /* Focus indicators / / WRONG: Removing focus outline */ :focus { outline: none; }

    /* RIGHT: Custom visible focus */ :focus { outline: 2px solid #2563eb; outline-offset: 2px; }

    :focus:not(:focus-visible) { outline: none; /* Hide for mouse, show for keyboard */ }

    :focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; }

anti_patterns:

  • name: Div Button description: Using div or span as clickable elements instead of button why: | Divs aren't focusable, don't respond to Enter/Space, aren't announced as buttons, and aren't included in form submission. You have to manually add all of this with JavaScript and ARIA. instead: | <button type="button" onclick="doAction()">Click me</button>

    If you need a link that looks like a button: <a href="/page" class="button">Go to page</a>

  • name: Mouse-Only Interactions description: Features that only work with hover or click why: | Keyboard users can't hover. Touch users have no hover state. One-arm users might use keyboard only. Screen reader users navigate via keyboard. instead: | Make all interactions work with:

    • Mouse (click, hover)
    • Keyboard (Enter, Space, Tab)
    • Touch (tap)

    Tooltips should be focusable: <button aria-describedby="tooltip"> Info </button>

    <div role="tooltip" id="tooltip"> Additional information </div>
  • name: Removing Focus Outline description: Using outline:none without replacement why: | Focus indicators tell keyboard users where they are. Without them, keyboard navigation is impossible. It's like making cursor invisible for mouse users. instead: | /* Provide custom focus styles */ :focus-visible { outline: 2px solid #2563eb; outline-offset: 2px; border-radius: 2px; }

  • name: Auto-Playing Media description: Audio or video that plays automatically why: | Unexpected sound is jarring. Screen reader users can't hear their screen reader over your video. Users with PTSD or anxiety can be triggered. It's also just annoying. instead: | <video controls>...</video>

    If autoplay is required (muted background): <video autoplay muted playsinline> <track kind="captions" src="..." /> </video> <button onclick="toggleAudio()">Enable sound</button>

  • name: ARIA First description: Adding ARIA to fix non-semantic HTML why: | ARIA is a repair tool for when native HTML isn't enough. Semantic HTML works out of the box. ARIA requires correct usage, testing, and maintenance. Wrong ARIA is worse than no ARIA. instead: |

    <!-- WRONG: ARIA bandaid --> <div role="navigation" aria-label="Main"> <div role="list"> <div role="listitem"><a href="/">Home</a></div> </div> </div> <!-- RIGHT: Semantic HTML --> <nav aria-label="Main"> <ul> <li><a href="/">Home</a></li> </ul> </nav>

handoffs: receives_from: - skill: frontend receives: Components to make accessible - skill: tailwind-ui receives: Styling system for accessible design

hands_to: - skill: testing provides: Accessibility test requirements - skill: frontend provides: Accessible component patterns

tags:

  • accessibility
  • a11y
  • wcag
  • aria
  • screen-reader
  • keyboard
  • inclusive
  • semantic-html