Accessibility Quick Audit: Find A11y Issues in Seconds
A practical Fusebox guide to accessibility quick audit.
Accessibility Quick Audit: Find A11y Issues in Seconds
Published: January 2024
Reading time: 8 minutes
Accessibility isn't just about compliance - it's about reaching all users. Here's how to quickly audit any website for common accessibility issues and understand their impact.
Why Accessibility Matters
- 1 in 4 adults have a disability
- $13 trillion spending power of disabled people globally
- Legal requirements in many countries (ADA, WCAG)
- Better SEO - accessible sites rank higher
- Better UX for everyone (captions help in noisy environments)
Quick Console Accessibility Audit
1. The 30-Second Audit
// Paste this for instant a11y check
(function quickA11yAudit() {
console.log('π Accessibility Quick Audit\n');
// 1. Images without alt text
const imagesNoAlt = Array.from(document.images).filter(img => !img.alt);
console.log(`β Images without alt text: ${imagesNoAlt.length}`);
if (imagesNoAlt.length > 0) {
console.log(' First few:', imagesNoAlt.slice(0, 3).map(img => img.src));
}
// 2. Headings hierarchy
const headings = Array.from(document.querySelectorAll('h1,h2,h3,h4,h5,h6'));
const h1Count = document.querySelectorAll('h1').length;
console.log(`${h1Count === 1 ? 'β
' : 'β'} H1 tags: ${h1Count} (should be 1)`);
// 3. Form labels
const inputs = Array.from(document.querySelectorAll('input, select, textarea'));
const unlabeled = inputs.filter(input => {
return !input.labels?.length &&
!input.getAttribute('aria-label') &&
!input.getAttribute('aria-labelledby');
});
console.log(`${unlabeled.length === 0 ? 'β
' : 'β'} Unlabeled form fields: ${unlabeled.length}`);
// 4. Buttons with no text
const emptyButtons = Array.from(document.querySelectorAll('button')).filter(btn => {
return !btn.textContent.trim() && !btn.getAttribute('aria-label');
});
console.log(`${emptyButtons.length === 0 ? 'β
' : 'β'} Empty buttons: ${emptyButtons.length}`);
// 5. Color contrast check (basic)
const lowContrast = [];
document.querySelectorAll('*').forEach(el => {
const style = window.getComputedStyle(el);
const bg = style.backgroundColor;
const fg = style.color;
if (bg !== 'rgba(0, 0, 0, 0)' && fg && bg !== fg) {
// Basic check - would need full algorithm for accuracy
lowContrast.push(el);
}
});
// 6. Links audit
const links = Array.from(document.querySelectorAll('a'));
const badLinks = links.filter(link => {
const text = link.textContent.trim();
return text === 'click here' || text === 'here' || text === 'read more';
});
console.log(`${badLinks.length === 0 ? 'β
' : 'β'} Links with vague text: ${badLinks.length}`);
console.log('\nπ‘ Run full audit with axe DevTools for detailed results');
})();
2. Keyboard Navigation Test
// Test keyboard accessibility
(function testKeyboardNav() {
let tabIndex = 0;
const focusableElements = [];
// Find all focusable elements
const selector = 'a, button, input, select, textarea, [tabindex]:not([tabindex="-1"])';
document.querySelectorAll(selector).forEach(el => {
if (!el.disabled) {
focusableElements.push({
element: el,
tag: el.tagName,
text: el.textContent?.trim() || el.value || el.alt || 'No text',
visible: el.offsetParent !== null
});
}
});
console.log('πΉ Keyboard Navigation Test');
console.log(`Total focusable elements: ${focusableElements.length}`);
console.log(`Hidden but focusable: ${focusableElements.filter(e => !e.visible).length}`);
// Check tab order
const tabOrders = focusableElements
.filter(e => e.element.tabIndex > 0)
.sort((a, b) => a.element.tabIndex - b.element.tabIndex);
if (tabOrders.length > 0) {
console.warn('β οΈ Custom tab order detected (can confuse users)');
console.table(tabOrders.map(e => ({
element: e.tag,
tabIndex: e.element.tabIndex,
text: e.text
})));
}
})();
Common Accessibility Issues
1. Missing Alternative Text
Problem:
<!-- Screen readers say "image" -->
<img src="product.jpg">
<!-- Decorative images announce filename -->
<img src="hero-banner-2024-v3-final.jpg">
Solution:
<!-- Informative image -->
<img src="product.jpg" alt="Blue ceramic coffee mug with company logo">
<!-- Decorative image -->
<img src="divider.png" alt="" role="presentation">
<!-- Complex image -->
<img src="chart.png" alt="Sales chart showing 25% growth">
<p id="chart-desc">Detailed description: Sales increased from...</p>
2. Poor Color Contrast
Testing contrast:
// Get contrast ratio between two colors
function getContrastRatio(color1, color2) {
// Convert to RGB
const rgb1 = getRGB(color1);
const rgb2 = getRGB(color2);
// Calculate relative luminance
const lum1 = getLuminance(rgb1);
const lum2 = getLuminance(rgb2);
// Calculate contrast ratio
const lighter = Math.max(lum1, lum2);
const darker = Math.min(lum1, lum2);
return (lighter + 0.05) / (darker + 0.05);
}
// WCAG Requirements:
// Normal text: 4.5:1
// Large text (18pt+): 3:1
// UI components: 3:1
Common failures:
/* Bad: Light gray on white (2.1:1) */
.bad {
color: #999;
background: #fff;
}
/* Good: Dark gray on white (7:1) */
.good {
color: #333;
background: #fff;
}
3. Missing Form Labels
Problem:
<!-- Screen reader: "Edit text" -->
<input type="email" placeholder="Enter your email">
<!-- Placeholder disappears when typing -->
<input type="password" placeholder="Password">
Solution:
<!-- Method 1: Visible label -->
<label for="email">Email address</label>
<input type="email" id="email">
<!-- Method 2: Screen reader only -->
<label>
<span class="sr-only">Email address</span>
<input type="email">
</label>
<!-- Method 3: ARIA label -->
<input type="email" aria-label="Email address">
4. Inaccessible Modals
Common modal mistakes:
// Bad modal implementation
function openModal() {
document.querySelector('.modal').style.display = 'block';
// Problems:
// - No focus management
// - No keyboard trap
// - Background still interactive
// - No ARIA attributes
}
Accessible modal:
function openAccessibleModal() {
const modal = document.querySelector('.modal');
const firstFocusable = modal.querySelector('button, [href], input, select, textarea');
// 1. Show modal
modal.style.display = 'block';
modal.setAttribute('aria-hidden', 'false');
// 2. Set focus
firstFocusable?.focus();
// 3. Trap focus
modal.addEventListener('keydown', (e) => {
if (e.key === 'Tab') {
// Handle tab cycling within modal
trapFocus(e, modal);
}
if (e.key === 'Escape') {
closeModal();
}
});
// 4. Hide background from screen readers
document.querySelector('main').setAttribute('aria-hidden', 'true');
}
5. Missing Skip Links
Problem: Keyboard users must tab through entire navigation
Solution:
<!-- Skip to main content link -->
<a href="#main" class="skip-link">Skip to main content</a>
<nav>
<!-- Long navigation menu -->
</nav>
<main id="main">
<!-- Main content -->
</main>
<style>
.skip-link {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
}
</style>
ARIA Usage Audit
1. Common ARIA Mistakes
// Find ARIA misuse
(function auditARIA() {
console.log('π·οΈ ARIA Usage Audit\n');
// Check for redundant ARIA
const redundant = [];
// button role on button element
document.querySelectorAll('button[role="button"]').forEach(el => {
redundant.push({ element: el, issue: 'Redundant role="button" on <button>' });
});
// aria-label same as text content
document.querySelectorAll('[aria-label]').forEach(el => {
if (el.textContent.trim() === el.getAttribute('aria-label')) {
redundant.push({ element: el, issue: 'aria-label duplicates visible text' });
}
});
console.log(`Redundant ARIA: ${redundant.length} issues`);
// Check for missing required ARIA
const missing = [];
// role="navigation" without aria-label
document.querySelectorAll('[role="navigation"]').forEach(el => {
if (!el.getAttribute('aria-label') && !el.getAttribute('aria-labelledby')) {
missing.push({ element: el, issue: 'Navigation landmark needs label' });
}
});
console.log(`Missing ARIA: ${missing.length} issues`);
})();
2. Landmarks Check
// Audit page landmarks
(function checkLandmarks() {
const landmarks = {
header: document.querySelectorAll('header, [role="banner"]').length,
nav: document.querySelectorAll('nav, [role="navigation"]').length,
main: document.querySelectorAll('main, [role="main"]').length,
footer: document.querySelectorAll('footer, [role="contentinfo"]').length
};
console.log('πΊοΈ Page Landmarks:');
console.log(`Header: ${landmarks.header} ${landmarks.header === 1 ? 'β
' : 'β οΈ'}`);
console.log(`Navigation: ${landmarks.nav}`);
console.log(`Main: ${landmarks.main} ${landmarks.main === 1 ? 'β
' : 'β οΈ'}`);
console.log(`Footer: ${landmarks.footer} ${landmarks.footer === 1 ? 'β
' : 'β οΈ'}`);
})();
Dynamic Content Accessibility
1. Live Regions
<!-- Announce changes to screen readers -->
<div aria-live="polite" aria-atomic="true">
<p>Form saved successfully</p>
</div>
<!-- Urgent announcements -->
<div role="alert">
Error: Invalid email address
</div>
<!-- Status updates -->
<div role="status" aria-live="polite">
Loading results... (3 of 10)
</div>
2. Dynamic Form Validation
// Accessible error messaging
function showError(input, message) {
// Create error element
const errorId = input.id + '-error';
let error = document.getElementById(errorId);
if (!error) {
error = document.createElement('span');
error.id = errorId;
error.className = 'error-message';
input.parentNode.insertBefore(error, input.nextSibling);
}
// Update error
error.textContent = message;
// Connect to input
input.setAttribute('aria-describedby', errorId);
input.setAttribute('aria-invalid', 'true');
// Announce to screen readers
error.setAttribute('role', 'alert');
}
Mobile Accessibility
Touch Target Testing
// Check touch target sizes
(function checkTouchTargets() {
const minSize = 44; // iOS recommendation
const smallTargets = [];
document.querySelectorAll('a, button, input, select, textarea').forEach(el => {
const rect = el.getBoundingClientRect();
if (rect.width < minSize || rect.height < minSize) {
smallTargets.push({
element: el.tagName,
size: `${Math.round(rect.width)}x${Math.round(rect.height)}`,
text: el.textContent?.trim() || el.value || 'No text'
});
}
});
if (smallTargets.length > 0) {
console.log(`β οΈ Small touch targets (< ${minSize}px):`);
console.table(smallTargets);
}
})();
Testing Tools Integration
1. Axe-core Integration
// Load axe-core for detailed testing
const script = document.createElement('script');
script.src = 'https://cdnjs.cloudflare.com/ajax/libs/axe-core/4.7.2/axe.min.js';
script.onload = () => {
axe.run().then(results => {
console.log('π Axe Accessibility Results:');
console.log(`Violations: ${results.violations.length}`);
console.log(`Passes: ${results.passes.length}`);
if (results.violations.length > 0) {
results.violations.forEach(violation => {
console.group(`β ${violation.help}`);
console.log(`Impact: ${violation.impact}`);
console.log(`Elements: ${violation.nodes.length}`);
console.log('WCAG:', violation.tags.join(', '));
console.groupEnd();
});
}
});
};
document.head.appendChild(script);
2. Screen Reader Testing
// Simulate screen reader experience
(function screenReaderView() {
// Create overlay showing screen reader interpretation
const overlay = document.createElement('div');
overlay.style.cssText = `
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: black;
color: white;
padding: 20px;
z-index: 999999;
overflow: auto;
font-family: monospace;
`;
// Extract screen reader visible content
const content = [];
// Get all text content
document.querySelectorAll('*').forEach(el => {
if (el.offsetParent !== null) { // Visible
const text = el.textContent?.trim();
const alt = el.alt;
const label = el.getAttribute('aria-label');
if (text) content.push(text);
if (alt) content.push(`[Image: ${alt}]`);
if (label) content.push(`[${label}]`);
}
});
overlay.innerHTML = `
<h2>Screen Reader View</h2>
<button onclick="this.parentElement.remove()">Close</button>
<pre>${content.join('\n')}</pre>
`;
document.body.appendChild(overlay);
})();
Accessibility Checklist
Critical Issues
- All images have appropriate alt text
- Color contrast meets WCAG standards
- All functionality keyboard accessible
- Forms have proper labels
- Page has proper heading hierarchy
Important Issues
- Focus indicators visible
- Error messages clearly associated
- Skip navigation links present
- ARIA used correctly
- Touch targets 44x44px minimum
Enhanced Experience
- Animations respect prefers-reduced-motion
- Content works at 200% zoom
- Videos have captions
- Complex images have long descriptions
- Language declared in HTML
The Business Case
Legal: Avoid lawsuits (increasing every year) Market: Reach 25% more users SEO: Google prioritizes accessible sites UX: Benefits all users (mobile, temporary disabilities) Brand: Shows you care about inclusion
The Bottom Line
Accessibility is not optional:
- It's a legal requirement
- It's good business
- It's better UX
- It's the right thing to do
Start with the basics. Fix critical issues. Build inclusively.
Audit accessibility instantly: Fusebox runs quick accessibility checks on any website while you browse. Find and understand a11y issues immediately. $29 one-time purchase.