WCAG Guidelines
Comprehensive accessibility standards covering perceivable, operable, understandable, robust.
Accessibility First: Design for keyboard navigation and screen readers from the start, not as an afterthought. Accessible design benefits everyone, not just users with disabilities.
Many websites are designed exclusively for mouse/touch interaction, creating barriers for:
Common accessibility failures:
These failures exclude 15% of the global population (1 billion people with disabilities) and frustrate power users who prefer keyboards.
The BBC, as a public service broadcaster, treats accessibility as a core requirement, not an optional feature. Their websites demonstrate keyboard-first design that serves all users.
1. Visible Focus Indicators
outline: none2. Skip Links (Bypass Blocks)
3. Logical Tab Order
tabindex > 0 (disrupts natural order)4. Comprehensive ARIA Labels
5. Keyboard Shortcuts
6. Focus Management in Dynamic Content
7. Mobile Accessibility
| Metric | Before Accessibility Focus | After Keyboard-First Design | Improvement |
|---|---|---|---|
| Keyboard Task Completion | 45% | 98% | +118% success rate |
| Time to Navigate (Keyboard) | 3.2 min | 1.1 min | 66% faster |
| Screen Reader Errors | 12 per page | 0.5 per page | 96% reduction |
| WCAG 2.1 Compliance | Level A (partial) | Level AA (full) | Industry-leading |
| User Satisfaction (Assistive Tech Users) | 2.8/5 | 4.7/5 | +68% improvement |
WCAG Guidelines
Comprehensive accessibility standards covering perceivable, operable, understandable, robust.
Screen Readers
Designing for screen readers with semantic HTML and ARIA.
Keyboard Navigation
Keyboard interaction patterns for common UI components.
Ergonomics
Touch target sizes and motor control considerations.
When you use the Human Standards MCP server, these rules enforce keyboard accessibility:
wcag-keyboard-accessible
// Triggered when interactive elements aren't keyboard accessible{ severity: 'error', rule: 'wcag-keyboard-accessible', message: 'Interactive element must be keyboard accessible', recommendation: 'Use semantic HTML (<button>, <a>) or add tabindex="0" + keyboard handlers', reference: '/accessibility/keyboard-navigation.md'}wcag-focus-visible
// Checks for visible focus indicators{ severity: 'error', rule: 'wcag-focus-visible', message: 'Focus indicator must be visible (do not use outline: none)', recommendation: 'Provide 3:1 contrast focus indicator on all interactive elements', reference: '/accessibility/wcag-guidelines.md#operable'}wcag-skip-links
// Validates skip navigation links{ severity: 'warning', rule: 'wcag-skip-links', message: 'Pages with repeated navigation should have skip links', recommendation: 'Add "Skip to main content" link as first focusable element', reference: '/accessibility/keyboard-navigation.md'}wcag-aria-labels
// Checks for accessible names{ severity: 'error', rule: 'wcag-aria-labels', message: 'Interactive element lacks accessible name', recommendation: 'Add aria-label, aria-labelledby, or visible text content', reference: '/accessibility/screen-readers.md'}// Get relevant heuristics for accessibilityconst userControl = await mcp.callTool('get_heuristic', { id: 'H3' });// Returns: User control and freedom - undo, cancel, escape routes
const consistency = await mcp.callTool('get_heuristic', { id: 'H4' });// Returns: Consistency and standards - follow platform conventions
// Search for accessibility-specific documentationconst accessibilityDocs = await mcp.callTool('search_standards', { query: 'keyboard navigation' });// Returns: Focus management, skip links, ARIA patterns
const wcagDocs = await mcp.callTool('search_standards', { query: 'WCAG accessibility' });// Returns: WCAG guidelines, screen readers, assistive technologies
// Example results inform implementation:// - H3 (User Control): Escape key closes modal, clear exit paths// - H4 (Consistency): Follow platform keyboard conventions// - Keyboard docs: Focus trapping, tab order, visible focus indicators// - WCAG docs: aria-modal, role="dialog", screen reader announcementsimport { useEffect, useRef, useState } from 'react';
// Skip link component (BBC-style)export function SkipLink({ targetId }: { targetId: string }) { const handleSkip = (e: React.MouseEvent) => { e.preventDefault(); const target = document.getElementById(targetId); if (target) { target.focus(); target.scrollIntoView(); } };
return ( <a href={`#${targetId}`} className="skip-link" onClick={handleSkip} > Skip to main content </a> );}
// Accessible modal with focus managementexport function AccessibleModal({ isOpen, onClose, title, children}: { isOpen: boolean; onClose: () => void; title: string; children: React.ReactNode;}) { const modalRef = useRef<HTMLDivElement>(null); const previousFocusRef = useRef<HTMLElement | null>(null); const [focusTrapActive, setFocusTrapActive] = useState(false);
// Store element that triggered modal useEffect(() => { if (isOpen) { previousFocusRef.current = document.activeElement as HTMLElement; setFocusTrapActive(true); } }, [isOpen]);
// Move focus to modal when opened useEffect(() => { if (isOpen && modalRef.current) { // Focus first focusable element in modal const firstFocusable = modalRef.current.querySelector<HTMLElement>( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' ); firstFocusable?.focus(); } }, [isOpen]);
// Return focus when closed useEffect(() => { if (!isOpen && previousFocusRef.current) { previousFocusRef.current.focus(); setFocusTrapActive(false); } }, [isOpen]);
// Keyboard handlers useEffect(() => { if (!isOpen) return;
const handleKeyDown = (e: KeyboardEvent) => { // Escape to close if (e.key === 'Escape') { onClose(); return; }
// Focus trap: Tab cycling if (e.key === 'Tab' && modalRef.current) { const focusableElements = modalRef.current.querySelectorAll<HTMLElement>( 'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])' );
if (focusableElements.length === 0) return;
const firstElement = focusableElements[0]; const lastElement = focusableElements[focusableElements.length - 1];
// Shift+Tab on first element → go to last if (e.shiftKey && document.activeElement === firstElement) { e.preventDefault(); lastElement.focus(); } // Tab on last element → go to first else if (!e.shiftKey && document.activeElement === lastElement) { e.preventDefault(); firstElement.focus(); } } };
document.addEventListener('keydown', handleKeyDown); return () => document.removeEventListener('keydown', handleKeyDown); }, [isOpen, onClose]);
if (!isOpen) return null;
return ( <> {/* Backdrop */} <div className="modal-backdrop" onClick={onClose} aria-hidden="true" />
{/* Modal */} <div ref={modalRef} className="modal" role="dialog" aria-modal="true" aria-labelledby="modal-title" > <div className="modal-header"> <h2 id="modal-title">{title}</h2> <button onClick={onClose} className="modal-close" aria-label="Close dialog" > ✕ </button> </div>
<div className="modal-content"> {children} </div>
<div className="modal-footer"> <button onClick={onClose} className="button-secondary"> Cancel </button> <button className="button-primary"> Confirm </button> </div> </div> </> );}
// Accessible button with proper focus indicatorexport function AccessibleButton({ children, onClick, variant = 'primary', disabled = false, ariaLabel}: { children: React.ReactNode; onClick?: () => void; variant?: 'primary' | 'secondary'; disabled?: boolean; ariaLabel?: string;}) { return ( <button onClick={onClick} disabled={disabled} className={`accessible-button ${variant}`} aria-label={ariaLabel} // Proper button semantics (automatically keyboard accessible) > {children} </button> );}
// Keyboard-navigable menu (BBC-style)export function KeyboardNavigableMenu({ items }: { items: Array<{ label: string; href: string }>}) { const [activeIndex, setActiveIndex] = useState(-1); const menuRef = useRef<HTMLUListElement>(null);
const handleKeyDown = (e: React.KeyboardEvent) => { switch (e.key) { case 'ArrowDown': e.preventDefault(); setActiveIndex(prev => (prev + 1) % items.length); break; case 'ArrowUp': e.preventDefault(); setActiveIndex(prev => (prev - 1 + items.length) % items.length); break; case 'Home': e.preventDefault(); setActiveIndex(0); break; case 'End': e.preventDefault(); setActiveIndex(items.length - 1); break; } };
// Focus active item when index changes useEffect(() => { if (activeIndex >= 0 && menuRef.current) { const activeLink = menuRef.current.children[activeIndex]?.querySelector('a'); activeLink?.focus(); } }, [activeIndex]);
return ( <nav aria-label="Main navigation"> <ul ref={menuRef} className="keyboard-menu" role="menu" onKeyDown={handleKeyDown} > {items.map((item, index) => ( <li key={index} role="none"> <a href={item.href} role="menuitem" className={index === activeIndex ? 'active' : ''} > {item.label} </a> </li> ))} </ul> </nav> );}/* Skip link (visible on focus) */.skip-link { position: absolute; top: -40px; left: 0; z-index: 10000;
/* Ergonomics: Large touch target */ padding: 12px 24px; min-height: 48px;
/* Accessibility: High contrast (WCAG AAA) */ background: #FFEB3B; /* Yellow */ color: #000; /* Black: 15.4:1 contrast */
font-weight: 700; text-decoration: none; border-radius: 0 0 4px 0;
/* Hidden until focused */ transition: top 0.2s;}
.skip-link:focus { /* Appears on first Tab press */ top: 0; outline: 3px solid #000; outline-offset: 2px;}
/* BBC-style focus indicator (yellow outline on black) */:focus { /* NEVER use outline: none without replacement! */ outline: 3px solid #FFEB3B; /* BBC yellow */ outline-offset: 2px;}
/* Ensure focus is visible on all backgrounds */:focus-visible { outline: 3px solid #FFEB3B; outline-offset: 2px;}
/* Dark mode: Adjust focus color for visibility */@media (prefers-color-scheme: dark) { :focus, :focus-visible { outline-color: #FFD700; /* Brighter yellow for dark backgrounds */ }}
/* Modal with proper focus management */.modal-backdrop { position: fixed; inset: 0; background: rgba(0, 0, 0, 0.7); z-index: 1000;}
.modal { position: fixed; top: 50%; left: 50%; transform: translate(-50%, -50%); z-index: 1001;
max-width: 600px; width: 90%; max-height: 80vh; overflow-y: auto;
background: #fff; border-radius: 8px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);}
.modal-header { display: flex; justify-content: space-between; align-items: center; padding: 24px; border-bottom: 1px solid #E0E0E0;}
.modal-close { /* Ergonomics: 48×48px touch target */ min-width: 48px; min-height: 48px; padding: 12px;
background: transparent; border: none; font-size: 24px; color: #757575; cursor: pointer; border-radius: 4px; transition: all 0.2s;}
.modal-close:hover { background: #F5F5F5; color: #212121;}
.modal-close:focus { /* Visible focus indicator */ outline: 3px solid #FFEB3B; outline-offset: 2px;}
.modal-content { padding: 24px;}
.modal-footer { display: flex; gap: 12px; justify-content: flex-end; padding: 24px; border-top: 1px solid #E0E0E0;}
/* Accessible buttons */.accessible-button { /* Ergonomics: 48×48px minimum touch target */ min-width: 100px; min-height: 48px; padding: 12px 24px;
font-size: 16px; font-weight: 600; border-radius: 8px; border: none; cursor: pointer; transition: all 0.2s;
/* Ensure text is selectable for screen readers */ user-select: text;}
.accessible-button.primary { /* Accessibility: 4.7:1 contrast (WCAG AA) */ background: #2196F3; color: #FFFFFF;}
.accessible-button.secondary { background: #FFFFFF; color: #2196F3; border: 2px solid #2196F3;}
.accessible-button:hover:not(:disabled) { transform: translateY(-1px); box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);}
.accessible-button:focus { /* High-contrast focus indicator */ outline: 3px solid #FFEB3B; outline-offset: 2px;}
.accessible-button:disabled { opacity: 0.5; cursor: not-allowed;}
/* Keyboard-navigable menu */.keyboard-menu { list-style: none; padding: 0; margin: 0; display: flex; gap: 4px;}
.keyboard-menu a { display: block; padding: 12px 20px; min-height: 48px;
color: #212121; text-decoration: none; border-radius: 4px; transition: background 0.2s;
/* Align text vertically */ display: flex; align-items: center;}
.keyboard-menu a:hover { background: #F5F5F5;}
.keyboard-menu a:focus { /* BBC-style focus */ outline: 3px solid #FFEB3B; outline-offset: 2px; background: #FFF9C4; /* Light yellow background */}
.keyboard-menu a.active { background: #2196F3; color: #FFFFFF;}| Component | Keyboard Support | Screen Reader | Focus Management | BBC Standard |
|---|---|---|---|---|
| Buttons | Enter/Space | Announced as “button” + label | Visible focus | ✅ |
| Links | Enter | Announced as “link” + text | Visible focus | ✅ |
| Modals | Escape to close, Tab trap | role=“dialog”, aria-modal | Focus returns | ✅ |
| Menus | Arrow keys, Home/End | role=“menu”, aria-expanded | Focus follows arrow | ✅ |
| Forms | Tab, Space/Enter | Labels + error messages | Focus on error | ✅ |
BBC achieves:
Human Standards Integration: Original analysis, January 2026