Accessibility Checker
A skill that audits and improves web accessibility (a11y) to ensure the portfolio website is usable by everyone, including people with disabilities.
Trigger Phrases
This skill activates when the user wants to check or improve accessibility:
- "check accessibility"
- "audit a11y"
- "fix accessibility issues"
- "make it accessible"
- "accessibility audit"
- "check WCAG compliance"
Instructions
When this skill is invoked, perform a comprehensive accessibility audit covering these key areas:
Step 1: Review the File to Audit
If the user specifies a file, read it first. Otherwise, ask which file or component needs to be audited.
Step 2: Color Contrast Audit (WCAG)
Check that all text meets WCAG color contrast requirements:
WCAG Standards
WCAG AA (Minimum):
- Normal text (< 18pt): 4.5:1 contrast ratio
- Large text (≥ 18pt or ≥ 14pt bold): 3:1 contrast ratio
- UI components and graphics: 3:1 contrast ratio
WCAG AAA (Enhanced):
- Normal text: 7:1 contrast ratio
- Large text: 4.5:1 contrast ratio
What to Check
- Text on backgrounds: Body text, headings, links, buttons
- Interactive elements: Button text, form labels, input text
- Dark mode: Ensure dark theme also meets contrast standards
- Disabled states: Even disabled elements should be distinguishable
- Icons and graphics: Non-decorative icons need sufficient contrast
Common Issues
/* ❌ BAD - Insufficient contrast */
.text {
color: #999999; /* Light gray */
background: #ffffff; /* White - only 2.8:1 contrast */
}
/* ✅ GOOD - Meets WCAG AA */
.text {
color: #525252; /* Dark gray */
background: #ffffff; /* White - 7.5:1 contrast */
}
/* ❌ BAD - Primary color might not have enough contrast */
.button {
color: #ffffff;
background: #4d94ff; /* Only 3.1:1 with white text */
}
/* ✅ GOOD - Use darker primary for better contrast */
.button {
color: #ffffff;
background: var(--color-primary-600); /* Better contrast */
}
Color Contrast Fixes
Provide specific fixes for any contrast issues:
- Darken light text or lighten dark backgrounds
- Use design system colors that meet standards
- Test in both light and dark modes
- Add text shadows for text over images (use with caution)
- Avoid pure gray (#808080) - use darker or lighter shades
Step 3: Keyboard Navigation Audit
Ensure all interactive elements are keyboard accessible:
What to Check
- Tab order: Logical flow through interactive elements
- Focus indicators: Visible focus states on all interactive elements
- Skip links: "Skip to main content" link for navigation bypass
- Trapped focus: Modals/dialogs trap and restore focus properly
- No keyboard traps: Users can navigate away from all elements
- Keyboard shortcuts: All mouse actions have keyboard equivalents
Common Issues
<!-- ❌ BAD - div used as button, not keyboard accessible -->
<div me</div>
<!-- ✅ GOOD - Semantic button, keyboard accessible -->
<button me</button>
<!-- ❌ BAD - No visible focus state -->
<style>
button:focus {
outline: none; /* Never do this! */
}
</style>
<!-- ✅ GOOD - Clear focus indicator -->
<style>
button:focus {
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
</style>
<!-- ❌ BAD - Missing skip link -->
<body>
<nav>...</nav>
<main>...</main>
</body>
<!-- ✅ GOOD - Skip link included -->
<body>
<a href="#main-content" class="skip-link">Skip to main content</a>
<nav>...</nav>
<main id="main-content">...</main>
</body>
Keyboard Navigation Fixes
- Add skip links for bypassing navigation
- Ensure visible focus states with proper outline or border
- Use semantic HTML (
<button>,<a>,<input>, etc.) - Test tab order - should be logical and sequential
- Handle modal focus - trap focus within modals, restore on close
- Add keyboard event handlers where needed
// ✅ GOOD - Handle both click and keyboard events
button.addEventListener('click', handleAction);
button.addEventListener('keydown', (e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
handleAction();
}
});
Skip Link Implementation
<a href="#main-content" class="skip-link">Skip to main content</a>
<style>
.skip-link {
position: absolute;
top: -40px;
left: 0;
background: var(--bg-primary);
color: var(--text-primary);
padding: var(--space-2) var(--space-4);
text-decoration: none;
border-radius: 0 0 4px 0;
z-index: 100;
}
.skip-link:focus {
top: 0;
outline: 2px solid var(--color-primary-500);
outline-offset: 2px;
}
</style>
Step 4: ARIA Labels & Semantic HTML Audit
Check for proper ARIA attributes and semantic markup:
What to Check
- Semantic HTML: Using proper HTML5 elements
- ARIA labels: Buttons, links, and icons have descriptive labels
- ARIA roles: Correct roles for custom widgets
- ARIA states:
aria-expanded,aria-hidden,aria-current, etc. - Landmark regions: Main, navigation, complementary, contentinfo
- Heading hierarchy: Proper H1 → H2 → H3 structure
- Form labels: All inputs have associated labels
Common Issues
<!-- ❌ BAD - Generic div without semantic meaning -->
<div class="navigation">
<div class="link">Home</div>
</div>
<!-- ✅ GOOD - Semantic HTML with proper roles -->
<nav role="navigation" aria-label="Main navigation">
<a href="#home">Home</a>
</nav>
<!-- ❌ BAD - Icon button without label -->
<button class="theme-toggle">
🌙
</button>
<!-- ✅ GOOD - Icon button with aria-label -->
<button class="theme-toggle" aria-label="Toggle dark mode">
<span aria-hidden="true">🌙</span>
</button>
<!-- ❌ BAD - Improper heading hierarchy -->
<h1>Page Title</h1>
<h3>Section Title</h3> <!-- Skipped H2! -->
<!-- ✅ GOOD - Proper heading hierarchy -->
<h1>Page Title</h1>
<h2>Section Title</h2>
<h3>Subsection Title</h3>
<!-- ❌ BAD - Input without label -->
<input type="email" placeholder="Email">
<!-- ✅ GOOD - Input with proper label -->
<label for="email">Email Address</label>
<input type="email" id="email" placeholder="you@example.com">
<!-- ❌ BAD - Decorative image with alt text -->
<img src="decorative-pattern.svg" alt="Decorative pattern">
<!-- ✅ GOOD - Decorative image with empty alt -->
<img src="decorative-pattern.svg" alt="">
<!-- OR use aria-hidden for purely decorative images -->
<img src="decorative-pattern.svg" aria-hidden="true" alt="">
ARIA Best Practices
- Use semantic HTML first: Don't use ARIA if HTML element exists
- Don't override semantics:
<button role="heading">is wrong - Required attributes: Some ARIA roles require specific attributes
- Dynamic content: Update ARIA states when content changes
- Landmark labels: Give unique labels to multiple landmarks
<!-- ✅ GOOD - Proper landmark structure -->
<header role="banner">
<nav role="navigation" aria-label="Main navigation">...</nav>
</header>
<main role="main" id="main-content">
<section aria-labelledby="projects-title">
<h2 id="projects-title">Projects</h2>
...
</section>
</main>
<aside role="complementary" aria-label="Related links">...</aside>
<footer role="contentinfo">...</footer>
Form Accessibility
<!-- ✅ GOOD - Accessible form with all necessary attributes -->
<form>
<div class="form-group">
<label for="name">
Name <span aria-label="required">*</span>
</label>
<input
type="text"
id="name"
name="name"
required
aria-required="true"
aria-describedby="name-hint"
>
<span id="name-hint" class="form-hint">Enter your full name</span>
</div>
<div class="form-group">
<label for="email">Email</label>
<input
type="email"
id="email"
name="email"
aria-describedby="email-error"
aria-invalid="false"
>
<span id="email-error" class="error" role="alert"></span>
</div>
<fieldset>
<legend>Preferred Contact Method</legend>
<label>
<input type="radio" name="contact" value="email"> Email
</label>
<label>
<input type="radio" name="contact" value="phone"> Phone
</label>
</fieldset>
<button type="submit">Send Message</button>
</form>
Step 5: Screen Reader Support Audit
Ensure the site works well with screen readers:
What to Check
- Alt text: All images have descriptive alt text (or empty for decorative)
- Link text: Links have descriptive text (not "click here")
- Button labels: Buttons describe their action
- Form labels: All inputs have associated labels
- Reading order: Content flows logically in DOM order
- Hidden content: Use
aria-hiddenfor decorative elements - Live regions: Use
aria-livefor dynamic content - Language: HTML
langattribute is set
Common Issues
<!-- ❌ BAD - Non-descriptive link text -->
<a href="/project1">Click here</a>
<!-- ✅ GOOD - Descriptive link text -->
<a href="/project1">View E-commerce Platform project</a>
<!-- ❌ BAD - Image without alt text -->
<img src="project-screenshot.jpg">
<!-- ✅ GOOD - Descriptive alt text -->
<img src="project-screenshot.jpg" alt="E-commerce platform homepage showing product grid and navigation">
<!-- ❌ BAD - Icon used for meaning without label -->
<button>
<svg>...</svg>
</button>
<!-- ✅ GOOD - Icon with descriptive label -->
<button aria-label="Open menu">
<svg aria-hidden="true">...</svg>
</button>
<!-- ❌ BAD - No language attribute -->
<html>
<!-- ✅ GOOD - Language specified -->
<html lang="en">
<!-- ❌ BAD - Dynamic content without announcement -->
<div id="notifications"></div>
<!-- ✅ GOOD - Dynamic content with live region -->
<div id="notifications" role="status" aria-live="polite" aria-atomic="true"></div>
Screen Reader Best Practices
- Meaningful alt text: Describe what the image conveys, not what it is
- Empty alt for decorative: Use
alt=""for purely decorative images - Descriptive links: Link text should make sense out of context
- Button vs link: Use
<button>for actions,<a>for navigation - Form feedback: Announce errors and success messages
- Loading states: Announce when content is loading
- Reading order: Ensure visual order matches DOM order
// ✅ GOOD - Announce dynamic content to screen readers
function showNotification(message) {
const notification = document.getElementById('notification');
notification.textContent = message;
notification.setAttribute('role', 'status');
notification.setAttribute('aria-live', 'polite');
}
// ✅ GOOD - Announce form errors
function showError(inputId, errorMessage) {
const input = document.getElementById(inputId);
const errorSpan = document.getElementById(`${inputId}-error`);
input.setAttribute('aria-invalid', 'true');
errorSpan.textContent = errorMessage;
errorSpan.setAttribute('role', 'alert');
// Focus the input for keyboard users
input.focus();
}
Step 6: Generate Accessibility Report
After auditing, provide a structured report:
Report Format
## Accessibility Audit Report
### Summary
- Issues found: [number]
- WCAG Level: [AA/AAA]
- Severity: [Critical/High/Medium/Low]
### Color Contrast Issues
1. [Component] - [Issue description]
- Current: [current values]
- Fix: [recommended fix]
### Keyboard Navigation Issues
1. [Component] - [Issue description]
- Fix: [recommended fix]
### ARIA & Semantic HTML Issues
1. [Component] - [Issue description]
- Fix: [recommended fix]
### Screen Reader Issues
1. [Component] - [Issue description]
- Fix: [recommended fix]
### Recommended Actions
1. [Priority 1 - Critical fixes]
2. [Priority 2 - Important fixes]
3. [Priority 3 - Nice-to-have improvements]
Step 7: Provide Fixes
For each issue found, provide the specific code fixes:
- Show the current problematic code
- Explain why it's an issue
- Provide the corrected code
- Explain the improvement
Accessibility Checklist
Use this comprehensive checklist for audits:
Structure & Semantics
- Valid, semantic HTML5
- Proper heading hierarchy (H1 → H2 → H3, no skips)
- Only one H1 per page
- Landmark regions used (
<header>,<nav>,<main>,<footer>) - Unique landmark labels if multiple of same type
- Language attribute set (
<html lang="en">) - Page has a descriptive title
Color & Contrast
- Text contrast meets WCAG AA (4.5:1 for normal, 3:1 for large)
- Interactive element contrast meets WCAG AA (3:1)
- Dark mode colors also meet contrast standards
- Color is not the only means of conveying information
- Link text is distinguishable from surrounding text
Keyboard Navigation
- All interactive elements are keyboard accessible
- Tab order is logical and sequential
- Focus indicators are clearly visible
- Skip to main content link provided
- No keyboard traps
- Modal focus is properly managed (trapped and restored)
- Dropdowns and menus are keyboard accessible
- Custom widgets have proper keyboard support
Images & Media
- All images have alt text (or alt="" for decorative)
- Alt text is descriptive and meaningful
- Decorative images use alt="" or aria-hidden="true"
- Icons have labels or are marked as decorative
- Complex images have longer descriptions
- Image-based links/buttons have descriptive alt text
Forms
- All inputs have associated labels
- Labels use
<label>element withforattribute - Required fields are marked (visually and with aria-required)
- Error messages are associated with inputs (aria-describedby)
- Error messages use role="alert"
- Fieldsets and legends used for grouped inputs
- Placeholder text is not used as the only label
- Form validation is accessible
Links & Buttons
- Link text is descriptive (not "click here")
- Links and buttons are distinguishable
- Icon buttons have aria-label
- Links that open new windows are indicated
- External links are indicated (visually or with aria-label)
- Current page is indicated in navigation (aria-current)
ARIA
- ARIA used appropriately (HTML semantics preferred)
- ARIA roles are correct
- Required ARIA attributes are present
- ARIA states updated when content changes
-
aria-labeloraria-labelledbyused appropriately -
aria-describedbyused for additional context -
aria-hiddenused for decorative content -
aria-liveused for dynamic content
Dynamic Content
- Loading states are announced
- Error messages are announced (role="alert")
- Success messages are announced
- Content changes are announced (aria-live)
- Dynamically added content is accessible
Responsive & Mobile
- Text can be resized to 200% without loss of content
- Site works in landscape and portrait orientations
- No horizontal scrolling at 320px width
- Touch targets are at least 44x44px
- Mobile menu is keyboard accessible
Testing Tools
Recommend using these tools for accessibility testing:
Automated Testing
- Lighthouse (Chrome DevTools): Built-in accessibility audit
- axe DevTools: Browser extension for accessibility testing
- WAVE: Web accessibility evaluation tool
- Pa11y: Command-line accessibility testing
- HTML Validator: Check for valid HTML
Manual Testing
- Keyboard navigation: Tab through entire page
- Screen reader: Test with NVDA (Windows), JAWS (Windows), or VoiceOver (Mac/iOS)
- Zoom: Test at 200% zoom
- Color contrast: Use Color Contrast Analyzer tool
- Mobile testing: Test on actual devices
Contrast Checkers
- WebAIM Contrast Checker: https://webaim.org/resources/contrastchecker/
- Contrast Ratio: https://contrast-ratio.com/
- Color Contrast Analyzer: Desktop app
Common Accessibility Patterns
Accessible Modal
<div class="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title">
<div class="modal-overlay"></div>
<div class="modal-content">
<h2 id="modal-title">Modal Title</h2>
<button class="modal-close" aria-label="Close modal">×</button>
<p>Modal content...</p>
<button>Confirm</button>
<button>Cancel</button>
</div>
</div>
<script>
// Trap focus within modal
function trapFocus(modal) {
const focusableElements = modal.querySelectorAll(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const firstElement = focusableElements[0];
const lastElement = focusableElements[focusableElements.length - 1];
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();
}
});
}
</script>
Accessible Dropdown Menu
<nav>
<button
aria-expanded="false"
aria-controls="dropdown-menu"
aria-haspopup="true"
>
Menu
</button>
<ul id="dropdown-menu" role="menu" hidden>
<li role="none">
<a href="#" role="menuitem">Item 1</a>
</li>
<li role="none">
<a href="#" role="menuitem">Item 2</a>
</li>
</ul>
</nav>
<script>
button.addEventListener('click', () => {
const isExpanded = button.getAttribute('aria-expanded') === 'true';
button.setAttribute('aria-expanded', !isExpanded);
menu.hidden = isExpanded;
});
</script>
Accessible Tabs
<div class="tabs">
<div role="tablist" aria-label="Project categories">
<button role="tab" aria-selected="true" aria-controls="panel-1" id="tab-1">
All
</button>
<button role="tab" aria-selected="false" aria-controls="panel-2" id="tab-2" tabindex="-1">
Web Apps
</button>
<button role="tab" aria-selected="false" aria-controls="panel-3" id="tab-3" tabindex="-1">
Mobile
</button>
</div>
<div role="tabpanel" id="panel-1" aria-labelledby="tab-1">
Content for All...
</div>
<div role="tabpanel" id="panel-2" aria-labelledby="tab-2" hidden>
Content for Web Apps...
</div>
<div role="tabpanel" id="panel-3" aria-labelledby="tab-3" hidden>
Content for Mobile...
</div>
</div>
Notes
- Accessibility is not optional - it's a legal requirement in many countries
- Good accessibility improves usability for everyone
- Test with real screen readers, not just automated tools
- Involve people with disabilities in testing when possible
- Accessibility should be considered from the start, not added at the end
- WCAG AA is the standard most organizations aim for
- Keep up with WCAG updates and best practices
- Document accessibility features and testing procedures