Skip to content

Accessibility - BBC GEL Keyboard Guidance

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:

  • Screen reader users (blind or low vision)
  • Keyboard-only users (motor disabilities, power users)
  • Switch device users (severe motor disabilities)
  • Voice control users (mobility impairments)
  • Temporary limitations (broken arm, repetitive strain injury)

Common accessibility failures:

  • No keyboard access to interactive elements
  • Invisible focus indicators (can’t tell where you are)
  • Illogical tab order (jumps around unpredictably)
  • Missing ARIA labels (screen readers can’t describe elements)
  • Keyboard traps (can enter but can’t exit)
  • Hidden “Skip to content” links (can’t bypass navigation)

These failures affect people who depend on keyboard or switch input, as well as people using a keyboard because of injury, context, or preference. BBC’s own focus guidance explicitly notes that “keyboard users” are not a single disability group and that keyboard access is an alternative to fine pointer control (BBC GEL: Focus).

The evidence for this case study is BBC’s published Global Experience Language (GEL) technical guidance. GEL describes framework-independent recommendations for BBC developers, contractors, and suppliers, with an explicit emphasis on the BBC Accessibility Guidelines (BBC GEL technical documentation). It documents intended patterns; it is not an audit of every BBC page or app.

1. Semantic controls before ARIA reconstruction

  • Use native links, buttons, and form controls for interactive content
  • Keep non-interactive elements out of the focus order
  • Rely on the built-in keyboard behavior and roles of semantic HTML where possible

BBC’s focus guide contrasts a scripted div role="button" with a native button, noting that the latter is focusable and responds to Enter and Space without added JavaScript (BBC GEL: Focus).

2. Visible, resilient focus treatment

  • Provide an obvious focus style wherever an interactive element can appear
  • Use a solid outline that remains visible against changing backgrounds
  • Account for Windows High Contrast Mode instead of relying only on background or color changes

The published GEL examples use a solid outline and describe focus styles as functional, not decorative (BBC GEL: Focus).

3. Logical focus order

  • Let source order determine the default sequence
  • Avoid positive tabindex values
  • Do not visually reorder interactive content in a way that contradicts source order

These constraints are stated directly in the focus guidance (BBC GEL: Focus order).

4. Bypass controls for repeated or dense interfaces

  • Provide a skip route to main content
  • Reveal visually hidden bypass links when they receive focus
  • Move focus to a real target so the bypass works for screen-reader and sighted keyboard users

BBC’s routing guide requires the main element to be reachable with a skip link, while the Share tools component demonstrates a focused “Skip sharing” link and focusable end target (BBC GEL: Routing, BBC GEL: Share tools).

5. Component semantics expose structure and state

  • Mark navigation as a labelled nav landmark
  • Use buttons for disclosure and expose state with aria-expanded
  • Identify the current page with aria-current

The GEL Site menu documents all three decisions and explains what common screen readers announce (BBC GEL: Site menu).

6. Restore focus after transient UI closes

  • Move focus into an action dialog when it opens
  • Make the surrounding interface inert while the dialog is active
  • Return focus to the invoking control when it closes
  • Use a live region, rather than stealing focus, for non-actionable status messages

This behavior is specified in the GEL Action dialog pattern (BBC GEL: Action dialogs).

The following are expected effects of the documented patterns, not measured outcomes for all BBC services.

  • Keyboard users get operable native controls and a predictable focus sequence
  • Screen reader users receive native roles, names, landmarks, and state
  • Switch device users benefit from the same sequential interaction path
  • Voice control users benefit from controls with programmatic names
  • Power users can use a keyboard without a parallel interaction model
  • Sighted keyboard users can see location through explicit focus treatment
  • Users of magnification encounter focus in an order aligned with layout
  • All users get controls whose roles and states follow platform conventions
  • Reusable expectations across teams and suppliers
  • Testable behavior for focus order, activation, and focus return
  • More robust controls by preferring platform semantics
  • Shared vocabulary for design, engineering, accessibility, and QA
  • Semantic HTML reduces the custom scripting needed to reconstruct native behavior
  • Shared patterns reduce one-off component decisions
  • Keyboard testing exposes order, visibility, activation, and recovery defects
  • Framework-independent guidance can be applied across multiple implementations

BBC’s public GEL material establishes the intended keyboard patterns and supplies reference implementations. It does not publish the before/after task-completion, navigation-time, screen-reader-error, or satisfaction figures previously shown on this page. Those figures have therefore been removed.

To measure an implementation of this pattern, report:

MeasureEvidence to collect
Keyboard task completionModerated or unmoderated task test, with product surface and sample stated
Focus-order defectsAudit findings by component and viewport
Keyboard trapsReproducible defect count, including browser and assistive technology
Time to bypass repeated contentTask timing with and without the bypass control
User confidenceSurvey instrument, sample, and uncertainty—not an unattributed score

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: '/code-design-tokens/aria-keyboard-patterns/'
}

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/#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: '/code-design-tokens/aria-keyboard-patterns/'
}

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/assistive-technologies/#screen-readers'
}
// Get relevant heuristics for accessibility
const 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 documentation
const 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 announcements
import { useEffect, useRef, useState } from 'react';
// Skip link component based on the documented pattern
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 management
export 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 indicator
export 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 based on the documented pattern
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>
);
}

CSS Example (High-Contrast Focus Indicators)

Section titled “CSS Example (High-Contrast Focus Indicators)”
/* 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;
}
/* Example high-contrast focus indicator */
: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 {
/* Example high-contrast focus */
outline: 3px solid #FFEB3B;
outline-offset: 2px;
background: #FFF9C4; /* Light yellow background */
}
.keyboard-menu a.active {
background: #2196F3;
color: #FFFFFF;
}
  • Text alternatives for non-text content (alt text)
  • Captions for audio/video
  • Color is not the only visual means of conveying information
  • Text contrast ≥4.5:1 (3:1 for large text)
  • All functionality available from keyboard
  • No keyboard traps (can navigate in and out)
  • Visible focus indicator (3:1 contrast)
  • Skip links to bypass repetitive content
  • Touch targets ≥44×44px (mobile)
  • Consistent navigation across pages
  • Descriptive labels on form fields
  • Error messages are specific and helpful
  • Logical reading order (tab order follows visual order)
  • Valid HTML (semantic markup)
  • ARIA attributes used correctly
  • Compatible with assistive technologies (screen readers, switch devices)
ComponentKeyboard SupportScreen ReaderFocus ManagementCheck
ButtonsEnter/SpaceAnnounced as “button” + labelVisible focus
LinksEnterAnnounced as “link” + textVisible focus
ModalsOperable controls; no page-level keyboard traprole="dialog" + name/descriptionFocus enters and returns
MenusArrow keys, Home/Endrole=“menu”, aria-expandedFocus follows arrow
FormsTab, Space/EnterLabels + error messagesFocus on error

Treat this table as a QA checklist, not a claim that every BBC product has been audited by Human Standards. The cited GEL pages are the evidence for the intended patterns; results for a particular service still require testing that service.



Human Standards Integration: Analysis updated against cited primary documentation, August 2026