Back to blog
InspectionJanuary 1, 2024Β· 7 min read

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');
}

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.