
WCAG 2.4.11 Focus Not Obscured (Minimum): Complete Implementation Guide
WCAG 2.4.11 Focus Not Obscured (Minimum) is a new Level AA success criterion introduced in WCAG 2.2. This requirement ensures that keyboard focus indicators are not completely hidden by other content, making keyboard navigation usable for everyone.
This is a Level AA requirement, meaning compliance is legally required under ADA, Section 508, and the European Accessibility Act (EAA).
What Does WCAG 2.4.11 Require?
Official Definition:
When a user interface component receives keyboard focus, the component is not entirely hidden due to author-created content.
In Plain English: When you tab through a page with your keyboard, the focused element must not be completely hidden behind sticky headers, cookie banners, chat widgets, or other overlays.
Key Points:
- β Minimum (Level AA): Focus must not be entirely hidden (partial obscuring is allowed)
- β Level AAA (2.4.12): Focus must not be obscured at all (stricter requirement)
- β Only applies to author-created content (not browser UI like autocomplete dropdowns)
- β Only applies when the component initially receives focus (not after scrolling)
Why This Matters
Real-World Impact:
Problem Scenario
User tabs through a form on mobile
β Focus lands on email input field
β Sticky header completely covers the focused field
β User can't see where they are or what they're typing
β Form becomes unusable via keyboard
Who This Helps:
- Keyboard-only users: Power users, motor impairments, assistive technology users
- Screen magnifier users: Already working with limited viewport, can't afford obscured focus
- Mobile users: Sticky headers are common, viewports are small
- Users with cognitive disabilities: Need to see focus to understand current position
Common Violations:
- Sticky headers covering top-of-page links
- Cookie consent banners hiding form fields
- Chat widgets obscuring navigation
- Fixed footers covering bottom elements
- Modal dialogs with fixed headers hiding focused content
Common Failure Patterns
β Failure 1: Sticky Header Covers Focus
<header style="position: sticky; top: 0; height: 80px; background: white; z-index: 1000;">
<nav>...</nav>
</header>
<main style="padding-top: 20px;"> <!-- Not enough padding! -->
<a href="#content">Skip to content</a> <!-- Gets hidden under header -->
<h1>Page Title</h1>
</main>
Problem: When user tabs to "Skip to content" link, it's completely hidden under the 80px sticky header.
β Failure 2: Cookie Banner Hides Form Fields
<div style="position: fixed; bottom: 0; left: 0; right: 0; height: 100px; background: #333; z-index: 9999;">
<p>We use cookies...</p>
<button>Accept</button>
</div>
<form>
<label for="email">Email:</label>
<input id="email" type="email"> <!-- Can be hidden under banner -->
</form>
Problem: Form fields near bottom of viewport get completely covered by fixed cookie banner.
β Failure 3: Chat Widget Obscures Navigation
<!-- Chat widget fixed to bottom-right -->
<div id="chat-widget" style="position: fixed; bottom: 20px; right: 20px; width: 300px; height: 400px; z-index: 10000;">
<iframe src="chat.html"></iframe>
</div>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li> <!-- Can be obscured by chat -->
</ul>
</nav>
β Solution 1: CSS Scroll Padding
The most effective solution for sticky headers:
/* Method 1: scroll-padding-top */
html {
scroll-padding-top: 100px; /* Height of sticky header + buffer */
}
/* Method 2: scroll-margin on focusable elements */
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus,
[tabindex]:not([tabindex="-1"]):focus {
scroll-margin-top: 120px; /* Header height + 20px buffer */
}
How It Works:
scroll-padding-top: Creates invisible padding at the top of the scroll containerscroll-margin-top: Creates invisible margin around focused elements- When element receives focus, browser automatically scrolls to ensure visibility
Browser Support: β All modern browsers (Chrome 69+, Firefox 68+, Safari 14.1+)
β Solution 2: JavaScript Focus Management
For complex layouts or dynamic headers:
// Ensure focused element is visible
function ensureFocusVisible() {
document.addEventListener('focusin', (event) => {
const focused = event.target;
const headerHeight = document.querySelector('header').offsetHeight;
const rect = focused.getBoundingClientRect();
// Check if focused element is obscured by sticky header
if (rect.top < headerHeight) {
// Scroll element into view with offset for header
const offset = rect.top - headerHeight - 20; // 20px buffer
window.scrollBy({
top: offset,
behavior: 'smooth'
});
}
});
}
// Initialize on page load
document.addEventListener('DOMContentLoaded', ensureFocusVisible);
Advanced Version with Bottom Overlays:
function ensureFocusVisible() {
document.addEventListener('focusin', (event) => {
const focused = event.target;
const rect = focused.getBoundingClientRect();
const viewportHeight = window.innerHeight;
// Get heights of fixed/sticky elements
const header = document.querySelector('header[style*="sticky"], header[style*="fixed"]');
const footer = document.querySelector('.cookie-banner, .chat-widget');
const headerHeight = header ? header.offsetHeight : 0;
const footerHeight = footer ? footer.offsetHeight : 0;
// Check top obscurity
if (rect.top < headerHeight) {
window.scrollBy({
top: rect.top - headerHeight - 20,
behavior: 'smooth'
});
}
// Check bottom obscurity
if (rect.bottom > viewportHeight - footerHeight) {
window.scrollBy({
top: rect.bottom - viewportHeight + footerHeight + 20,
behavior: 'smooth'
});
}
});
}
β Solution 3: Proper Spacing
Ensure adequate padding/margin around focusable content:
/* Add padding to main content equal to sticky header height */
header.sticky {
position: sticky;
top: 0;
height: 80px;
z-index: 1000;
}
main {
padding-top: 100px; /* 80px header + 20px buffer */
}
/* For fixed footers/banners */
.cookie-banner {
position: fixed;
bottom: 0;
height: 100px;
z-index: 9999;
}
body {
padding-bottom: 120px; /* 100px banner + 20px buffer */
}
β Solution 4: Smart Cookie Banners
Auto-dismiss or reposition on focus:
// Move cookie banner when it would obscure focus
const cookieBanner = document.querySelector('.cookie-banner');
document.addEventListener('focusin', (event) => {
if (!cookieBanner) return;
const focused = event.target;
const focusRect = focused.getBoundingClientRect();
const bannerRect = cookieBanner.getBoundingClientRect();
// Check if banner overlaps focused element
const overlaps = !(
focusRect.bottom < bannerRect.top ||
focusRect.top > bannerRect.bottom ||
focusRect.right < bannerRect.left ||
focusRect.left > bannerRect.right
);
if (overlaps) {
// Temporarily hide or reposition banner
cookieBanner.style.transform = 'translateY(100%)';
// Restore after focus moves
setTimeout(() => {
cookieBanner.style.transform = '';
}, 5000);
}
});
Implementation Checklist
1. Audit Your Sticky/Fixed Elements
// Find all sticky/fixed elements on your page
function findStickyElements() {
const allElements = document.querySelectorAll('*');
const stickyElements = [];
allElements.forEach(el => {
const style = window.getComputedStyle(el);
if (style.position === 'sticky' || style.position === 'fixed') {
stickyElements.push({
element: el,
height: el.offsetHeight,
position: style.position,
selector: el.tagName + (el.id ? '#' + el.id : '') + (el.className ? '.' + el.className.replace(/\s+/g, '.') : '')
});
}
});
console.table(stickyElements);
return stickyElements;
}
// Run in browser console
findStickyElements();
2. Calculate Required Offsets
// Calculate total offset needed for top and bottom
function calculateOffsets() {
const topFixed = document.querySelectorAll('[style*="position: fixed"][style*="top:"], [style*="position: sticky"][style*="top:"]');
const bottomFixed = document.querySelectorAll('[style*="position: fixed"][style*="bottom:"]');
const topOffset = Array.from(topFixed).reduce((total, el) => total + el.offsetHeight, 0);
const bottomOffset = Array.from(bottomFixed).reduce((total, el) => total + el.offsetHeight, 0);
console.log(`Top offset needed: ${topOffset + 20}px`);
console.log(`Bottom offset needed: ${bottomOffset + 20}px`);
return { top: topOffset + 20, bottom: bottomOffset + 20 };
}
3. Implement CSS Solution
/* Add to your global stylesheet */
:root {
--header-height: 80px;
--footer-height: 100px;
--buffer: 20px;
}
html {
scroll-padding-top: calc(var(--header-height) + var(--buffer));
scroll-padding-bottom: calc(var(--footer-height) + var(--buffer));
}
/* Individual element margins as fallback */
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus,
[role="button"]:focus,
[role="link"]:focus,
[tabindex]:not([tabindex="-1"]):focus {
scroll-margin-top: calc(var(--header-height) + var(--buffer));
scroll-margin-bottom: calc(var(--footer-height) + var(--buffer));
}
4. Test with Keyboard Navigation
Manual Testing Procedure:
- Navigate with Tab key through entire page
- Check each focusable element:
- Is any part of the focus indicator visible?
- Can you see the focused element's content?
- Is the element at least partially visible?
- Test at different viewport sizes:
- Desktop (1920x1080)
- Tablet (768x1024)
- Mobile (375x667)
- Test with browser zoom:
- 100%, 200%, 400% zoom levels
- Test with common overlays:
- Cookie banners present
- Chat widgets open
- Mobile navigation expanded
Testing Tools
Automated Testing
// Test script for 2.4.11 compliance
function test2411Compliance() {
const focusableElements = document.querySelectorAll(
'a[href], button, input, select, textarea, [tabindex]:not([tabindex="-1"])'
);
const violations = [];
focusableElements.forEach((el, index) => {
// Give focus
el.focus();
// Get element position
const rect = el.getBoundingClientRect();
// Check if completely hidden by checking all four corners
const corners = [
{ x: rect.left, y: rect.top },
{ x: rect.right, y: rect.top },
{ x: rect.left, y: rect.bottom },
{ x: rect.right, y: rect.bottom }
];
const allCornersObscured = corners.every(corner => {
const topElement = document.elementFromPoint(corner.x, corner.y);
return topElement !== el && !el.contains(topElement);
});
if (allCornersObscured) {
violations.push({
element: el,
selector: el.tagName + (el.id ? '#' + el.id : ''),
text: el.textContent?.trim().substring(0, 50),
position: { top: rect.top, left: rect.left, bottom: rect.bottom, right: rect.right }
});
}
});
console.log(`Found ${violations.length} violations of 2.4.11:`);
console.table(violations);
return violations;
}
// Run test
test2411Compliance();
Browser DevTools
Chrome DevTools:
- Open DevTools (F12)
- Go to Elements tab
- Press Tab to navigate through page
- DevTools automatically highlights focused element
- Check if highlight is visible or obscured
Accessibility Insights:
- Install Accessibility Insights extension
- Run "Tab Stops" assessment
- Manually verify each focused element is visible
Common Mistakes to Avoid
β Mistake 1: Forgetting Mobile Viewports
/* This works on desktop but fails on mobile */
html {
scroll-padding-top: 80px; /* Desktop header height */
}
/* Mobile header is taller! */
@media (max-width: 768px) {
header {
height: 120px; /* Taller mobile header */
}
/* Forgot to update scroll-padding! */
}
β Fix:
html {
scroll-padding-top: 80px;
}
@media (max-width: 768px) {
html {
scroll-padding-top: 140px; /* 120px header + 20px buffer */
}
}
β Mistake 2: Not Accounting for z-index Stacking
/* Modal has higher z-index than cookie banner */
.modal {
position: fixed;
z-index: 10000;
}
.cookie-banner {
position: fixed;
bottom: 0;
z-index: 1000; /* Lower z-index but still obscures content BEHIND modal */
}
Problem: Cookie banner can still obscure focused elements even if modal is "above" it.
β Fix: Hide lower-priority overlays when higher-priority ones are active:
// Hide cookie banner when modal is open
function openModal() {
document.querySelector('.modal').classList.add('active');
document.querySelector('.cookie-banner').style.display = 'none';
}
function closeModal() {
document.querySelector('.modal').classList.remove('active');
document.querySelector('.cookie-banner').style.display = 'block';
}
β Mistake 3: Only Testing with Mouse
Many developers never test keyboard navigation and miss these issues entirely.
β Fix: Make keyboard testing part of your QA process:
- Test every page with keyboard only (no mouse)
- Use actual keyboard users for usability testing
- Add automated tests for focus visibility
Real-World Examples
β Good Example: GitHub
GitHub's sticky header automatically adjusts scroll position when tabbing through navigation:
/* Simplified version of GitHub's approach */
.Header {
position: sticky;
top: 0;
height: 64px;
z-index: 32;
}
html {
scroll-padding-top: 64px;
}
/* Additional margin on focusable elements */
.Header-link:focus {
scroll-margin-top: 80px; /* Extra buffer */
}
β Good Example: GOV.UK
GOV.UK uses JavaScript to manage focus visibility with cookie banners:
// Simplified version
const cookieBanner = document.querySelector('.cookie-banner');
const header = document.querySelector('.govuk-header');
document.addEventListener('focusin', (event) => {
const focusRect = event.target.getBoundingClientRect();
const headerRect = header.getBoundingClientRect();
const bannerRect = cookieBanner?.getBoundingClientRect();
// If focus would be obscured, scroll to make it visible
if (focusRect.top < headerRect.bottom + 10) {
event.target.scrollIntoView({ block: 'center', behavior: 'smooth' });
}
});
Implementation for Different Frameworks
React
import { useEffect } from 'react';
function FocusManager() {
useEffect(() => {
const handleFocus = (event) => {
const header = document.querySelector('header');
const headerHeight = header?.offsetHeight || 0;
const rect = event.target.getBoundingClientRect();
if (rect.top < headerHeight + 20) {
window.scrollBy({
top: rect.top - headerHeight - 20,
behavior: 'smooth'
});
}
};
document.addEventListener('focusin', handleFocus);
return () => document.removeEventListener('focusin', handleFocus);
}, []);
return null; // This component only handles focus logic
}
export default FocusManager;
Vue
<template>
<div class="app">
<!-- Your app content -->
</div>
</template>
<script>
export default {
mounted() {
this.initFocusManagement();
},
methods: {
initFocusManagement() {
document.addEventListener('focusin', (event) => {
const header = document.querySelector('header');
const headerHeight = header?.offsetHeight || 0;
const rect = event.target.getBoundingClientRect();
if (rect.top < headerHeight + 20) {
window.scrollBy({
top: rect.top - headerHeight - 20,
behavior: 'smooth'
});
}
});
}
}
};
</script>
WordPress
// Add to functions.php
function add_focus_management_styles() {
?>
<style>
html {
scroll-padding-top: 120px; /* Adjust for your theme's header */
}
a:focus,
button:focus,
input:focus,
select:focus,
textarea:focus {
scroll-margin-top: 140px;
}
</style>
<?php
}
add_action('wp_head', 'add_focus_management_styles');
How AllAccessible Helps
AllAccessible automatically detects and fixes focus obscurity issues:
Automatic Detection:
- Identifies sticky/fixed elements on your site
- Calculates required scroll offsets
- Detects when focus would be obscured
Automatic Fixes:
- Injects appropriate
scroll-paddingandscroll-marginCSS - Manages focus visibility with JavaScript fallback
- Handles dynamic content (modals, banners, chat widgets)
Real-Time Monitoring:
- Continuous testing of focus visibility
- Alerts when new violations are detected
- Automatic remediation across all pages
Start Free Trial - Fix focus visibility issues in minutes, not months.
Related Resources
π Master WCAG 2.2: Complete WCAG 2.2 Compliance Guide
Related Success Criteria:
- 2.4.12 Focus Not Obscured (Enhanced) - Level AAA version
- 2.4.13 Focus Appearance - Focus indicator design
- 2.4.7 Focus Visible - Ensure focus is always visible
Implementation Guides:
- Keyboard Navigation Complete Guide
- Focus Management in Single Page Applications
- Sticky Header Accessibility Best Practices
Summary
WCAG 2.4.11 Focus Not Obscured (Minimum) Requirements:
- β Level AA (legally required)
- β Focus must not be entirely hidden by author-created content
- β Applies to sticky headers, banners, widgets, modals
- β Partial obscuring is allowed (Minimum level)
Quick Implementation:
html {
scroll-padding-top: [header-height + 20px];
scroll-padding-bottom: [footer-height + 20px];
}
Test by: Tabbing through entire page and verifying every focused element is at least partially visible.
Need help implementing WCAG 2.4.11 across your entire site? AllAccessible automatically detects and fixes focus visibility issues - starting at $10/month.