Banner is keyboard navigable, screen reader compatible, WCAG 2.1 AA
Why it matters
A consent banner that is keyboard-inaccessible is functionally invisible to screen reader users and keyboard-only navigators, forcing them to accept cookies by default or abandon the site. WCAG 2.2 SC 2.1.1 requires all functionality to be operable via keyboard; SC 4.1.2 requires interactive controls to have accessible names. Section 508 (2018 refresh 502.3.1) applies the same standard to US government-adjacent products. The legal exposure compounds: a banner that traps or loses keyboard focus also violates WCAG 2.2 SC 2.1.2 (no keyboard trap), creating both accessibility and consent-validity failures simultaneously.
Severity rationale
Medium because keyboard and screen-reader inaccessibility violates WCAG 2.2 SC 2.1.1 and 4.1.2, but does not directly invalidate the consent mechanism for sighted mouse users who can still interact with it.
Remediation
Add role="dialog", aria-modal="true", focus management on mount, and a manual focus trap. All checkboxes must have associated labels.
'use client'
import { useEffect, useRef } from 'react'
export function CookieBanner({ onSave }: { onSave: (s: ConsentState) => void }) {
const bannerRef = useRef<HTMLDivElement>(null)
const firstBtn = useRef<HTMLButtonElement>(null)
useEffect(() => { firstBtn.current?.focus() }, [])
function handleKeyDown(e: React.KeyboardEvent) {
if (e.key !== 'Tab') return
const els = bannerRef.current?.querySelectorAll<HTMLElement>(
'button, input, a[href], [tabindex]:not([tabindex="-1"])'
)
if (!els?.length) return
const first = els[0], last = els[els.length - 1]
if (e.shiftKey && document.activeElement === first) { e.preventDefault(); last.focus() }
else if (!e.shiftKey && document.activeElement === last) { e.preventDefault(); first.focus() }
}
return (
<div ref={bannerRef} role="dialog" aria-label="Cookie consent"
aria-modal="true" onKeyDown={handleKeyDown}
className="fixed bottom-0 inset-x-0 bg-white border-t p-4">
<button ref={firstBtn} onClick={() => onSave({ analytics: true, marketing: true })}>
Accept all
</button>
<button onClick={() => onSave({ analytics: false, marketing: false })}>
Reject all
</button>
</div>
)
}
Verify color contrast separately: text-gray-400 on bg-white fails the WCAG 4.5:1 threshold for normal text.
DetectionPro
The detection method for this pattern is part of AuditBuffet Pro.
The full method (what to look for, pass and fail conditions, and how results get reported) ships inside the Cookie & Consent Compliance Audit. The pattern definition, severity rationale, and remediation remain openly licensed under CC-BY-4.0.
External references
- wcag:2.2 · 4.1.2 — Name, Role, Value — interactive controls must have programmatic name and role
- wcag:2.2 · 2.1.1 — Keyboard — all functionality operable by keyboard
- wcag:2.2 · 1.4.3 — Contrast (Minimum) — 4.5:1 ratio for normal text
- section-508:2018-refresh · 502.3.1 — Objectification — accessible name and role for UI components
Taxons
History
- 2026-04-18·v1.0.0·Initial import from cookie-consent-compliance·automated