@bernierllc/validators-a11y-focus-order
v0.6.0
Published
Accessibility focus order validation for the BernierLLC validators ecosystem - validates keyboard navigation, tab order, and focus visibility
Readme
@bernierllc/validators-a11y-focus-order
Accessibility focus order validation for keyboard navigation and WCAG compliance. Part of the BernierLLC validators ecosystem.
Overview
This validator ensures proper keyboard navigation and focus management in HTML by checking:
- Tab Index Validation - Detects positive tabindex values that disrupt natural tab order
- Focus Visibility - Ensures focusable elements have visible focus indicators
- Keyboard Trap Detection - Identifies potential keyboard traps where focus cannot escape
- Skip Link Validation - Validates skip link targets and implementation
Installation
npm install @bernierllc/validators-a11y-focus-orderQuick Start
import { validateA11yFocusOrder } from '@bernierllc/validators-a11y-focus-order';
// Validate HTML for focus order issues
const problems = await validateA11yFocusOrder({
html: '<button tabindex="5">Click me</button>',
validateTabIndex: true,
validateFocusVisibility: true,
validateKeyboardTraps: true,
validateSkipLinks: true
});
problems.forEach(problem => {
console.log(`${problem.severity}: ${problem.message}`);
console.log(`Suggestion: ${problem.suggestion}`);
});Usage
Basic Validation
import { validateA11yFocusOrder } from '@bernierllc/validators-a11y-focus-order';
const html = `
<nav>
<a href="#main">Skip to main content</a>
<button tabindex="3">Bad practice</button>
</nav>
<main id="main">Content</main>
`;
const problems = await validateA11yFocusOrder({ html });
// Output:
// error: Element has positive tabindex="3" which disrupts natural keyboard navigation orderDetailed Analysis
import { checkFocusOrder } from '@bernierllc/validators-a11y-focus-order';
const result = checkFocusOrder({
html: document.body.innerHTML,
validateTabIndex: true,
validateFocusVisibility: true,
validateKeyboardTraps: true,
validateSkipLinks: true
});
console.log(`Total focusable elements: ${result.totalFocusableElements}`);
console.log(`Elements with positive tabindex: ${result.positiveTabIndices.length}`);
console.log(`Missing focus indicators: ${result.missingFocusVisibility.length}`);
console.log(`Potential keyboard traps: ${result.keyboardTraps.length}`);
console.log(`Skip link issues: ${result.skipLinkIssues.length}`);
console.log(`All checks pass: ${result.passes}`);Selective Validation
// Only validate tab index usage
const problems = await validateA11yFocusOrder({
html,
validateTabIndex: true,
validateFocusVisibility: false,
validateKeyboardTraps: false,
validateSkipLinks: false
});
// Only validate skip links
const skipLinkProblems = await validateA11yFocusOrder({
html,
validateTabIndex: false,
validateSkipLinks: true
});Custom Tab Index Tolerance
// Allow positive tabindex up to 3
const problems = await validateA11yFocusOrder({
html,
validateTabIndex: true,
maxPositiveTabIndex: 3
});Validation Rules
1. Positive Tab Index (a11y-focus-order/positive-tabindex)
WCAG Reference: 2.4.3 Focus Order
Positive tabindex values (tabindex="1", tabindex="2", etc.) disrupt the natural document flow and create confusing navigation patterns for keyboard users.
Bad Practice:
<button tabindex="5">First button</button>
<button tabindex="1">Second button (focused first!)</button>
<button>Third button (focused last)</button>Best Practice:
<!-- Natural tab order follows DOM order -->
<button>First button</button>
<button>Second button</button>
<button>Third button</button>
<!-- Use tabindex="0" to make non-focusable elements focusable -->
<div tabindex="0" role="button">Custom button</div>
<!-- Use tabindex="-1" for programmatic focus only -->
<div tabindex="-1" id="error-message">Error: form invalid</div>2. Missing Focus Visibility (a11y-focus-order/missing-focus-visibility)
WCAG Reference: 2.4.7 Focus Visible
All focusable elements must have visible focus indicators so keyboard users can see which element has focus.
Bad Practice:
<button style="outline:none">No visible focus indicator</button>
<a href="#" style="outline:0">Link without focus indicator</a>Best Practice:
<!-- Use default focus outline -->
<button>Default focus outline</button>
<!-- Custom focus styles with :focus or :focus-visible -->
<button class="custom-focus">Custom focus indicator</button>
<style>
.custom-focus:focus {
outline: 2px solid blue;
outline-offset: 2px;
}
/* Modern approach with :focus-visible */
.custom-focus:focus-visible {
box-shadow: 0 0 0 3px rgba(0, 100, 255, 0.5);
}
</style>3. Keyboard Trap Detection (a11y-focus-order/keyboard-trap)
WCAG Reference: 2.1.2 No Keyboard Trap
Users must be able to navigate away from any component using only the keyboard. Trapping focus violates accessibility requirements.
Bad Practice:
<div onkeydown="if(event.key === 'Tab') event.preventDefault()">
Trapped content
</div>Best Practice:
<!-- Modal with proper escape mechanism -->
<div role="dialog" aria-labelledby="modal-title">
<h2 id="modal-title">Modal Dialog</h2>
<button>Action</button>
<button onclick="closeModal()">Close</button>
</div>
<script>
function setupModal() {
document.addEventListener('keydown', (event) => {
if (event.key === 'Escape') {
closeModal();
}
});
}
</script>4. Invalid Skip Links (a11y-focus-order/invalid-skip-link)
WCAG Reference: 2.4.1 Bypass Blocks
Skip links must point to valid targets within the page to help keyboard users bypass repetitive navigation.
Bad Practice:
<!-- Target doesn't exist -->
<a href="#main-content">Skip to main content</a>
<main id="content">Content here</main>
<!-- Not a fragment identifier -->
<a href="/main">Skip to main</a>Best Practice:
<!-- Valid skip link with matching target -->
<a href="#main-content" class="skip-link">Skip to main content</a>
<main id="main-content">
<h1>Main Content</h1>
<p>Content here...</p>
</main>
<style>
/* Visually hidden but available to screen readers and keyboard users */
.skip-link {
position: absolute;
left: -10000px;
top: auto;
width: 1px;
height: 1px;
overflow: hidden;
}
.skip-link:focus {
position: static;
width: auto;
height: auto;
}
</style>API Reference
validateA11yFocusOrder(input, utils?)
Main validation function that checks all focus order rules.
Parameters:
input: FocusOrderCheckInput- Configuration objecthtml: string- HTML content to validate (required)validateTabIndex?: boolean- Check for positive tabindex values (default: true)validateFocusVisibility?: boolean- Check for missing focus indicators (default: true)validateKeyboardTraps?: boolean- Check for keyboard traps (default: true)validateSkipLinks?: boolean- Validate skip link targets (default: true)maxPositiveTabIndex?: number- Maximum allowed positive tabindex (default: 0)
utils?: SharedUtils- Optional shared utilities
Returns: Promise<Problem[]> - Array of validation problems
checkFocusOrder(input)
Analyzes HTML and returns detailed focus order metrics.
Parameters:
input: FocusOrderCheckInput- Configuration object
Returns: FocusOrderCheckResult
passes: boolean- Whether all checks passpositiveTabIndices: FocusableElement[]- Elements with positive tabindexmissingFocusVisibility: FocusableElement[]- Elements without focus indicatorskeyboardTraps: FocusableElement[]- Potential keyboard trapsskipLinkIssues: FocusableElement[]- Invalid skip linkstotalFocusableElements: number- Total focusable elements found
Individual Rules
Each rule can be imported and used separately:
import {
positiveTabindex,
missingFocusVisibility,
keyboardTrap,
invalidSkipLink
} from '@bernierllc/validators-a11y-focus-order';Integration with BernierLLC Ecosystem
With Validators Runner
import { createRunner } from '@bernierllc/validators-runner';
import { a11yFocusOrderValidator } from '@bernierllc/validators-a11y-focus-order';
const runner = createRunner({
validators: [a11yFocusOrderValidator]
});
const results = await runner.validate({
html: document.body.innerHTML
});With Custom Reporters
import { createConsoleReporter } from '@bernierllc/validators-reporters';
import { validateA11yFocusOrder } from '@bernierllc/validators-a11y-focus-order';
const problems = await validateA11yFocusOrder({ html });
const reporter = createConsoleReporter();
reporter.report(problems);Real-World Examples
Navigation with Skip Link
const html = `
<body>
<a href="#main" class="skip-link">Skip to main content</a>
<nav>
<ul>
<li><a href="/">Home</a></li>
<li><a href="/about">About</a></li>
</ul>
</nav>
<main id="main">
<h1>Welcome</h1>
</main>
</body>
`;
const result = checkFocusOrder({ html, validateSkipLinks: true });
// result.passes === true
// result.skipLinkIssues.length === 0Form with Proper Focus Management
const html = `
<form>
<label for="username">Username:</label>
<input id="username" type="text" />
<label for="password">Password:</label>
<input id="password" type="password" />
<button type="submit">Log In</button>
<button type="button" tabindex="-1" id="hidden-helper">
Helper (programmatic focus only)
</button>
</form>
`;
const result = checkFocusOrder({ html });
// result.passes === true
// result.totalFocusableElements === 4
// result.positiveTabIndices.length === 0Modal Dialog with Escape Handling
const html = `
<div role="dialog" aria-labelledby="dialog-title" aria-modal="true">
<h2 id="dialog-title">Confirm Action</h2>
<p>Are you sure?</p>
<button onclick="confirm()">Yes</button>
<button onclick="cancel()">No</button>
</div>
`;
const problems = await validateA11yFocusOrder({
html,
validateKeyboardTraps: true
});
// Should not flag properly implemented modalsWCAG Compliance
This validator helps ensure compliance with:
- WCAG 2.1.2: No Keyboard Trap (Level A)
- WCAG 2.4.1: Bypass Blocks (Level A)
- WCAG 2.4.3: Focus Order (Level A)
- WCAG 2.4.7: Focus Visible (Level AA)
Best Practices
Avoid Positive Tab Index: Never use positive tabindex values. Use DOM order for natural tab sequence.
Always Provide Focus Indicators: Ensure all interactive elements have visible focus states.
Test with Keyboard Only: Verify your site is fully navigable using only the keyboard.
Implement Skip Links: Provide skip links to bypass repetitive navigation.
Avoid Keyboard Traps: Ensure users can always escape from modals and components.
Use Semantic HTML: Prefer native focusable elements (button, a, input) over custom implementations.
Integration Status
Logger Integration
Not applicable for primitive validators. This package provides pure validation functions without side effects. Logging is handled by the calling application or validators-runner.
Docs-Suite Integration
Ready - Complete markdown documentation and API documentation available via TypeDoc. All functions are fully documented with JSDoc comments for automatic documentation generation.
NeverHub Integration
Not applicable for primitive validators. NeverHub integration is handled at the runner/orchestrator level. This package is designed to be framework-agnostic and can be used standalone or through validators-runner with NeverHub support.
License
Copyright (c) 2025 Bernier LLC. All rights reserved.
This package is part of the BernierLLC validators ecosystem for accessibility and WCAG compliance validation.
See Also
- @bernierllc/validators-core - Core validation framework
- @bernierllc/validators-utils - Shared validator utilities
- @bernierllc/validators-a11y-contrast - Color contrast validation
- @bernierllc/validators-html-syntax - HTML syntax validation
