
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
-
Tab through entire page
-
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?
-
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:
- First: Implement 2.4.11 (Level AA) - legally required
- Then: Add generous buffers for 2.4.12 (Level AAA) - best practice
- 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:
- 2.4.11 Focus Not Obscured (Minimum) - Level AA version
- 2.4.13 Focus Appearance - Focus indicator design
- 2.4.7 Focus Visible - Basic focus visibility
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.