Skip to main content
Back to Blog
WCAG 2.2

WCAG 2.4.12 Focus Not Obscured (Enhanced): Complete Implementation Guide

Master WCAG 2.4.12 Focus Not Obscured (Enhanced) - Level AAA criterion. Learn how to ensure focused elements are never obscured by overlays, going beyond the Level AA minimum requirement.

AllAccessible
10 min read
WCAG 2.2Focus ManagementLevel AAAKeyboard Navigation
WCAG 2.4.12 Focus Not Obscured (Enhanced): Complete Implementation Guide

WCAG 2.4.12 Focus Not Obscured (Enhanced): Complete Implementation Guide

WCAG 2.4.12 Focus Not Obscured (Enhanced) is a new Level AAA success criterion introduced in WCAG 2.2. It's the stricter version of 2.4.11, requiring that focused elements be completely visible with no obscuring whatsoever.

This is a Level AAA requirement, meaning it's optional but represents best practices for exceptional accessibility.

What Does WCAG 2.4.12 Require?

Official Definition:

When a user interface component receives keyboard focus, the component is not entirely or partially hidden due to author-created content.

Difference from 2.4.11 (Level AA):

  • 2.4.11 (AA): Focus must not be entirely hidden (partial obscuring OK)
  • 2.4.12 (AAA): Focus must not be partially OR entirely hidden (no obscuring at all)

In Plain English: When you tab to an element, it must be completely visibleβ€”not even a tiny bit can be hidden behind sticky headers, cookie banners, or any other overlays.

Key Points:

  • βœ… Stricter than 2.4.11 - NO obscuring allowed
  • βœ… Focus must be 100% visible
  • βœ… Applies to sticky headers, footers, chat widgets, banners
  • βœ… Level AAA (optional but recommended for optimal UX)

Why This Matters

The Problem with Partial Obscuring:

Even if most of a focused element is visible (passing 2.4.11 AA), partial obscuring causes issues:

User tabs to "Submit" button
β†’ Top 10px hidden by sticky header (2.4.11 βœ… passes AA)
β†’ User sees "ubmit" instead of "Submit"
β†’ User unsure if correct element is focused
β†’ Reduced confidence, potential errors

Who Benefits from AAA Level:

  • Screen magnifier users: Working with limited viewport
  • Users with cognitive disabilities: Need complete visual clarity
  • Users with low vision: Partial obscuring creates confusion
  • Everyone: Better UX when focus is always 100% visible

Difference from Level AA (2.4.11)

Example: Sticky Header Scenario

<header style="position: sticky; top: 0; height: 80px; background: white; z-index: 1000;">
  <nav>Logo | Home | Products | Contact</nav>
</header>

<main style="padding-top: 20px;">
  <a href="#content">Skip to content</a> <!-- Link is 30px tall -->
</main>

When "Skip to content" link receives focus:

Scenario 1: Link is 30px tall, top 10px hidden by 80px header

  • 2.4.11 (AA): βœ… PASSES (20px visible out of 30px = partially visible)
  • 2.4.12 (AAA): ❌ FAILS (top 10px obscured = not 100% visible)

Scenario 2: Link is 30px tall, completely hidden by 80px header

  • 2.4.11 (AA): ❌ FAILS (0px visible = completely hidden)
  • 2.4.12 (AAA): ❌ FAILS (0px visible = completely hidden)

Scenario 3: Link is 30px tall, 100% visible with 100px top padding

  • 2.4.11 (AA): βœ… PASSES (completely visible)
  • 2.4.12 (AAA): βœ… PASSES (completely visible)

βœ… Solution: Ensure Complete Visibility

The key difference for AAA compliance is adding extra buffer space:

Level AA (2.4.11) Solution:

html {
  scroll-padding-top: 80px; /* Header height */
}

*:focus {
  scroll-margin-top: 80px;
}

Result: Element might be partially obscured but not entirely hidden.

Level AAA (2.4.12) Solution:

html {
  scroll-padding-top: 100px; /* Header height + generous buffer */
}

*:focus {
  scroll-margin-top: 120px; /* Extra buffer ensures complete visibility */
}

Result: Element guaranteed to be 100% visible with room to spare.

Implementation: AAA-Compliant Focus Management

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>2.4.12 AAA Compliance Example</title>

  <style>
    /* Sticky header */
    .site-header {
      position: sticky;
      top: 0;
      height: 80px;
      background: white;
      border-bottom: 2px solid #e0e0e0;
      z-index: 1000;
      display: flex;
      align-items: center;
      padding: 0 20px;
    }

    /* Fixed cookie banner */
    .cookie-banner {
      position: fixed;
      bottom: 0;
      left: 0;
      right: 0;
      height: 100px;
      background: #333;
      color: white;
      padding: 20px;
      z-index: 999;
    }

    /* AAA-COMPLIANT SCROLL PADDING */
    html {
      /* Header (80px) + generous buffer (40px) = 120px */
      scroll-padding-top: 120px;

      /* Cookie banner (100px) + generous buffer (40px) = 140px */
      scroll-padding-bottom: 140px;
    }

    /* AAA-COMPLIANT FOCUS MARGINS */
    a:focus,
    button:focus,
    input:focus,
    select:focus,
    textarea:focus,
    [tabindex]:not([tabindex="-1"]):focus {
      /* Top margin includes header + large buffer */
      scroll-margin-top: 140px;

      /* Bottom margin includes banner + large buffer */
      scroll-margin-bottom: 160px;

      /* Visible focus indicator */
      outline: 3px solid #0066cc;
      outline-offset: 2px;
    }

    /* Main content padding */
    main {
      /* Extra padding to ensure first element never obscured */
      padding-top: 140px;
      padding-bottom: 160px;
      min-height: 100vh;
    }

    /* Form elements */
    .form-group {
      margin-bottom: 24px;
    }

    label {
      display: block;
      margin-bottom: 8px;
      font-weight: 600;
    }

    input[type="text"],
    input[type="email"],
    select,
    textarea {
      width: 100%;
      max-width: 400px;
      padding: 12px;
      border: 2px solid #ccc;
      border-radius: 4px;
      font-size: 16px;
    }

    button {
      padding: 12px 24px;
      background: #0066cc;
      color: white;
      border: none;
      border-radius: 4px;
      font-size: 16px;
      cursor: pointer;
    }

    /* Mobile adjustments */
    @media (max-width: 768px) {
      .site-header {
        height: 120px; /* Taller mobile header */
      }

      html {
        scroll-padding-top: 160px; /* 120px header + 40px buffer */
      }

      *:focus {
        scroll-margin-top: 180px;
      }

      main {
        padding-top: 180px;
      }
    }
  </style>
</head>
<body>
  <header class="site-header">
    <nav>
      <a href="/">Home</a> |
      <a href="/products">Products</a> |
      <a href="/about">About</a> |
      <a href="/contact">Contact</a>
    </nav>
  </header>

  <main>
    <h1>AAA-Compliant Focus Visibility</h1>

    <p>Tab through this page and notice that every focused element is <strong>completely visible</strong> with no obscuring from the sticky header or cookie banner.</p>

    <form>
      <div class="form-group">
        <label for="name">Full Name</label>
        <input type="text" id="name" name="name" required>
      </div>

      <div class="form-group">
        <label for="email">Email Address</label>
        <input type="email" id="email" name="email" required>
      </div>

      <div class="form-group">
        <label for="country">Country</label>
        <select id="country" name="country">
          <option>United States</option>
          <option>Canada</option>
          <option>United Kingdom</option>
        </select>
      </div>

      <div class="form-group">
        <label for="message">Message</label>
        <textarea id="message" name="message" rows="5"></textarea>
      </div>

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

    <div style="height: 1000px;">
      <p>Scroll down to test bottom elements...</p>
    </div>

    <a href="#top">Back to top</a>
  </main>

  <div class="cookie-banner">
    <p>This website uses cookies. <a href="/privacy">Learn more</a></p>
    <button>Accept</button> <button>Decline</button>
  </div>

  <script>
    // Enhanced focus management for AAA compliance
    document.addEventListener('focusin', (event) => {
      const focused = event.target;

      // Get all fixed/sticky overlays
      const header = document.querySelector('.site-header');
      const cookieBanner = document.querySelector('.cookie-banner');

      const headerHeight = header ? header.offsetHeight : 0;
      const bannerHeight = cookieBanner && !cookieBanner.hidden
        ? cookieBanner.offsetHeight
        : 0;

      const focusedRect = focused.getBoundingClientRect();
      const viewportHeight = window.innerHeight;

      // AAA: Ensure COMPLETE visibility with generous buffers
      const topBuffer = 40;  // Extra buffer beyond header
      const bottomBuffer = 40; // Extra buffer beyond banner

      const requiredTopClearance = headerHeight + topBuffer;
      const requiredBottomClearance = bannerHeight + bottomBuffer;

      // Check if ANY part is obscured from top
      if (focusedRect.top < requiredTopClearance) {
        window.scrollBy({
          top: focusedRect.top - requiredTopClearance,
          behavior: 'smooth'
        });
      }

      // Check if ANY part is obscured from bottom
      if (focusedRect.bottom > viewportHeight - requiredBottomClearance) {
        window.scrollBy({
          top: focusedRect.bottom - (viewportHeight - requiredBottomClearance),
          behavior: 'smooth'
        });
      }

      // Announce focus change to screen readers
      const announcement = `Focused on ${focused.getAttribute('aria-label') || focused.textContent || focused.type}`;
      announceToScreenReader(announcement);
    });

    function announceToScreenReader(message) {
      const liveRegion = document.getElementById('focus-announcer') ||
        createLiveRegion();

      liveRegion.textContent = message;
    }

    function createLiveRegion() {
      const liveRegion = document.createElement('div');
      liveRegion.id = 'focus-announcer';
      liveRegion.setAttribute('role', 'status');
      liveRegion.setAttribute('aria-live', 'polite');
      liveRegion.setAttribute('aria-atomic', 'true');
      liveRegion.style.position = 'absolute';
      liveRegion.style.left = '-10000px';
      liveRegion.style.width = '1px';
      liveRegion.style.height = '1px';
      liveRegion.style.overflow = 'hidden';
      document.body.appendChild(liveRegion);
      return liveRegion;
    }
  </script>
</body>
</html>

Key Differences for AAA Implementation

1. Larger Buffers

AA (2.4.11):

scroll-padding-top: 80px; /* Just header height */

AAA (2.4.12):

scroll-padding-top: 120px; /* Header + 40px buffer */

2. More Generous Margins

AA (2.4.11):

*:focus {
  scroll-margin-top: 80px;
}

AAA (2.4.12):

*:focus {
  scroll-margin-top: 140px; /* Extra margin ensures complete visibility */
}

3. Proactive JavaScript

AAA compliance benefits from JavaScript that ensures complete visibility:

function ensureCompleteVisibility(element) {
  const rect = element.getBoundingClientRect();
  const viewportHeight = window.innerHeight;

  // Calculate all potential obscuring elements
  const obscuringElements = document.querySelectorAll('[style*="fixed"], [style*="sticky"]');

  let topObscured = 0;
  let bottomObscured = 0;

  obscuringElements.forEach(overlay => {
    const overlayRect = overlay.getBoundingClientRect();

    // Check top obscurity
    if (overlayRect.bottom > rect.top && overlayRect.top < rect.top) {
      topObscured = Math.max(topObscured, overlayRect.bottom);
    }

    // Check bottom obscurity
    if (overlayRect.top < rect.bottom && overlayRect.bottom > rect.bottom) {
      bottomObscured = Math.max(bottomObscured, viewportHeight - overlayRect.top);
    }
  });

  // Scroll if ANY part is obscured
  if (topObscured > 0 || bottomObscured > 0) {
    element.scrollIntoView({
      block: 'center', // Center ensures complete visibility
      behavior: 'smooth'
    });
  }
}

Testing for 2.4.12 (AAA) Compliance

Manual Testing

  1. Tab through entire page

  2. For each focused element, verify:

    • Is 100% of the element visible?
    • Is any pixel obscured by header/footer/overlay?
    • Can you see the complete focus indicator?
  3. Test edge cases:

    • First focusable element on page
    • Last focusable element on page
    • Elements near sticky headers
    • Elements near cookie banners
    • Mobile viewport (smaller screen)

Visual Testing Tool

// Highlight any focused element that's partially obscured
document.addEventListener('focusin', (e) => {
  const focused = e.target;
  const rect = focused.getBoundingClientRect();

  // Check all four corners
  const corners = [
    { x: rect.left, y: rect.top }, // Top-left
    { x: rect.right, y: rect.top }, // Top-right
    { x: rect.left, y: rect.bottom }, // Bottom-left
    { x: rect.right, y: rect.bottom } // Bottom-right
  ];

  const obscuredCorners = corners.filter(corner => {
    const topElement = document.elementFromPoint(corner.x, corner.y);
    return topElement !== focused && !focused.contains(topElement);
  });

  if (obscuredCorners.length > 0) {
    // AAA FAILURE: Partially obscured
    focused.style.boxShadow = '0 0 0 4px red';
    console.warn('2.4.12 AAA FAILURE: Focused element partially obscured');
    console.log('Obscured corners:', obscuredCorners.length, 'of 4');
  } else {
    // AAA PASS: Completely visible
    focused.style.boxShadow = '0 0 0 4px green';
    console.log('2.4.12 AAA PASS: Focused element completely visible');
  }
});

When to Implement AAA (2.4.12)

Recommended For:

βœ… High-traffic public websites (government, healthcare, education) βœ… Sites with complex overlays (many sticky elements, chat widgets) βœ… Sites targeting users with disabilities βœ… Enterprise applications with power users βœ… Any site aiming for best-in-class accessibility

May Skip For:

⚠️ Small business websites (AA compliance may be sufficient) ⚠️ Simple layouts (no sticky headers or overlays) ⚠️ Resource-constrained projects (focus on AA first)

Implementation Priority

Recommended Approach:

  1. First: Implement 2.4.11 (Level AA) - legally required
  2. Then: Add generous buffers for 2.4.12 (Level AAA) - best practice
  3. Finally: Add JavaScript fallback for edge cases

How AllAccessible Helps

AllAccessible tests for both AA and AAA focus obscurity:

Detection:

  • Tests for partial obscuring (AAA)
  • Tests for complete obscuring (AA)
  • Identifies buffer size needed
  • Checks across all viewport sizes

Fixes:

  • Calculates optimal scroll-padding values
  • Implements appropriate focus margins
  • Adds JavaScript fallbacks
  • Provides AAA-compliant solutions

Start Free Trial - Achieve AAA focus visibility - starting at $10/month.

Related Resources

πŸ“– Master WCAG 2.2: Complete WCAG 2.2 Compliance Guide

Related Success Criteria:

Summary

WCAG 2.4.12 Focus Not Obscured (Enhanced) Requirements:

  • βœ… Level AAA (optional best practice)
  • βœ… Focused elements must be 100% visible (no partial obscuring)
  • βœ… Stricter than 2.4.11 AA (which allows partial obscuring)
  • βœ… Requires generous buffers beyond overlay heights

Quick Implementation:

html {
  scroll-padding-top: [overlay-height + 40px];
  scroll-padding-bottom: [overlay-height + 40px];
}

*:focus {
  scroll-margin-top: [overlay-height + 60px];
  scroll-margin-bottom: [overlay-height + 60px];
}

Test by: Tabbing through page and verifying every focused element is 100% visible with no obscuring.

Need help achieving AAA focus visibility? AllAccessible automatically implements optimal buffers - starting at $10/month.

Share this article