Skip to main content

accessibility

Audit and improve web accessibility following WCAG 2.1 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".

Source facts

Repository
FlorianBruniaux/starmapper
Last source activity
May 17, 2026 at 12:10
Detected SKILL.md language
English
Stars
90
Forks
4

Install options

The review-first prompt is selected by default. You can switch to a direct command or download a local copy.

Review the source files

Read SKILL.md and any companion files shown by SkillsMP before deciding whether to install.

Showing SKILL.md

SKILL.md
Source instructions · Read-only preview
name
accessibility
description
Audit and improve web accessibility following WCAG 2.1 guidelines. Use when asked to "improve accessibility", "a11y audit", "WCAG compliance", "screen reader support", "keyboard navigation", or "make accessible".
license
MIT
metadata
{"author":"web-quality-skills","version":"1.0"}
version
1.0.0
# Accessibility (a11y) Comprehensive accessibility guidelines based on WCAG 2.1 and Lighthouse accessibility audits. Goal: make content usable by everyone, including people with disabilities. ## WCAG Principles: POUR | Principle | Description | |-----------|-------------| | **P**erceivable | Content can be perceived through different senses | | **O**perable | Interface can be operated by all users | | **U**nderstandable | Content and interface are understandable | | **R**obust | Content works with assistive technologies | ## Conformance levels | Level | Requirement | Target | |-------|-------------|--------| | **A** | Minimum accessibility | Must pass | | **AA** | Standard compliance | Should pass (legal requirement in many jurisdictions) | | **AAA** | Enhanced accessibility | Nice to have | --- ## Perceivable ### Text alternatives (1.1) **Images require alt text:** ```html <!-- ❌ Missing alt --> <img src="chart.png"> <!-- ✅ Descriptive alt --> <img src="chart.png" alt="Bar chart showing 40% increase in Q3 sales"> <!-- ✅ Decorative image (empty alt) --> <img src="decorative-border.png" alt="" role="presentation"> <!-- ✅ Complex image with longer description --> <figure> <img src="infographic.png" alt="2024 market trends infographic" aria-describedby="infographic-desc"> <figcaption id="infographic-desc"> <!-- Detailed description --> </figcaption> </figure> ``` **Icon buttons need accessible names:** ```html <!-- ❌ No accessible name --> <button><svg><!-- menu icon --></svg></button> <!-- ✅ Using aria-label --> <button aria-label="Open menu"> <svg aria-hidden="true"><!-- menu icon --></svg> </button> <!-- ✅ Using visually hidden text --> <button> <svg aria-hidden="true"><!-- menu icon --></svg> <span class="visually-hidden">Open menu</span> </button> ``` **Visually hidden class:** ```css .visually-hidden { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0, 0, 0, 0); white-space: nowrap; border: 0; } ``` ### Color contrast (1.4.3, 1.4.6) | Text Size | AA minimum | AAA enhanced | |-----------|------------|--------------| | Normal text (< 18px / < 14px bold) | 4.5:1 | 7:1 | | Large text (≥ 18px / ≥ 14px bold) | 3:1 | 4.5:1 | | UI components & graphics | 3:1 | 3:1 | ```css /* ❌ Low contrast (2.5:1) */ .low-contrast { color: #999; background: #fff; } /* ✅ Sufficient contrast (7:1) */ .high-contrast { color: #333; background: #fff; } /* ✅ Focus states need contrast too */ :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } ``` **Don't rely on color alone:** ```html <!-- ❌ Only color indicates error --> <input class="error-border"> <style>.error-border { border-color: red; }</style> <!-- ✅ Color + icon + text --> <div class="field-error"> <input aria-invalid="true" aria-describedby="email-error"> <span id="email-error" class="error-message"> <svg aria-hidden="true"><!-- error icon --></svg> Please enter a valid email address </span> </div> ``` --- ## Operable ### Keyboard accessible (2.1) **All functionality must be keyboard accessible:** ```javascript // ❌ Only handles click element.addEventListener('click', handleAction); // ✅ Handles both click and keyboard element.addEventListener('click', handleAction); element.addEventListener('keydown', (e) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); handleAction(); } }); ``` **No keyboard traps:** ```javascript // Modal focus management function openModal(modal) { const focusableElements = modal.querySelectorAll( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1]; // Trap focus within modal modal.addEventListener('keydown', (e) => { if (e.key === 'Tab') { if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } if (e.key === 'Escape') { closeModal(); } }); firstElement.focus(); } ``` ### Focus visible (2.4.7) ```css /* ❌ Never remove focus outlines */ *:focus { outline: none; } /* ✅ Use :focus-visible for keyboard-only focus */ :focus { outline: none; } :focus-visible { outline: 2px solid #005fcc; outline-offset: 2px; } /* ✅ Or custom focus styles */ button:focus-visible { box-shadow: 0 0 0 3px rgba(0, 95, 204, 0.5); } ``` ### Skip links (2.4.1) ```html <body> <a href="#main-content" class="skip-link">Skip to main content</a> <header><!-- navigation --></header> <main id="main-content" tabindex="-1"> <!-- main content --> </main> </body> ``` ```css .skip-link { position: absolute; top: -40px; left: 0; background: #000; color: #fff; padding: 8px 16px; z-index: 100; } .skip-link:focus { top: 0; } ``` ### Motion (2.3) ```css /* Respect reduced motion preference */ @media (prefers-reduced-motion: reduce) { *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; scroll-behavior: auto !important; } } ``` --- ## Understandable ### Page language (3.1.1) ```html <!-- ❌ No language specified --> <html> <!-- ✅ Language specified --> <html lang="en"> ``` ### Form labels (3.3.2) ```html <!-- ❌ No label association --> <input type="email" placeholder="Email"> <!-- ✅ Explicit label --> <label for="email">Email address</label> <input type="email" id="email" name="email" autocomplete="email" required> <!-- ✅ With instructions --> <label for="password">Password</label> <input type="password" id="password" aria-describedby="password-requirements"> <p id="password-requirements"> Must be at least 8 characters with one number. </p> ``` ### Error handling (3.3.1, 3.3.3) ```html <!-- Announce errors to screen readers --> <form novalidate> <div class="field" aria-live="polite"> <label for="email">Email</label> <input type="email" id="email" aria-invalid="true" aria-describedby="email-error"> <p id="email-error" class="error" role="alert"> Please enter a valid email address (e.g., name@example.com) </p> </div> </form> ``` --- ## Robust ### Valid HTML (4.1.1) ```html <!-- ❌ Duplicate IDs --> <div id="content">...</div> <div id="content">...</div> <!-- ✅ Unique IDs --> <div id="main-content">...</div> <div id="sidebar-content">...</div> ``` ### ARIA usage (4.1.2) **Prefer native elements:** ```html <!-- ❌ ARIA role on div --> <div role="button" tabindex="0">Click me</div> <!-- ✅ Native button --> <button>Click me</button> ``` **When ARIA is needed:** ```html <!-- Custom tabs component --> <div role="tablist" aria-label="Map filters"> <button role="tab" id="tab-1" aria-selected="true" aria-controls="panel-1">Countries</button> <button role="tab" id="tab-2" aria-selected="false" aria-controls="panel-2" tabindex="-1">Cities</button> </div> ``` ### Live regions (4.1.3) ```html <!-- Status updates --> <div aria-live="polite" aria-atomic="true" class="status"> <!-- Content updates announced to screen readers --> </div> <!-- Urgent alerts --> <div role="alert" aria-live="assertive"> <!-- Interrupts current announcement --> </div> ``` ```javascript // Announce dynamic content changes const showNotification = (message, type = 'polite') => { const container = document.getElementById(`${type}-announcer`); container.textContent = ''; // Clear first requestAnimationFrame(() => { container.textContent = message; }); }; ``` --- ## StarMapper-specific notes - **MapLibre canvas**: the map itself is not keyboard-navigable (known limitation). Add an accessible text description of what the map shows. - **Popups**: MapLibre popups injected via `innerHTML` must sanitize content. Use `textContent` for user-provided data (location strings, usernames).
View on GitHub
This SKILL.md is very large, so SkillsMP previews the first section here. View on GitHub