The Problem
TypeScript prevents runtime type errors. It does not prevent accessibility violations. These patterns compile successfully but break for keyboard and screen reader users.
Missing Accessible Name
1// Compiles fine. TypeScript says nothing.2<button onClick={handleDelete}>3 <TrashIcon />4</button>Screen reader: "Button". What does this button do?
No Focus Trap
1// Compiles fine. No errors.2<div className="modal" onClick={handleClose}>3 <input autoFocus />4 <button>Save</button>5</div>Tab key exits the modal. Keyboard users lose context.
Missing Focus Management
1// Compiles fine. Where does focus go after close?2function Dialog({ onClose }: Props) {3 return <div role="dialog">...</div>;4}Focus restoration fails. Users don't know where they are.
Why This Happens
- →Feedback loop is weeks or months: Accessibility issues discovered during audits, user reports, or manual testing—long after code is written
- →No type-level enforcement: TypeScript catches type errors at compile time but treats accessibility as optional
- →Manual patterns are error-prone: Focus management, ARIA relationships, and keyboard handling require deep domain knowledge
What @a13y Does
Provides runtime utilities and React primitives that enforce accessibility requirements through TypeScript types and development-time validation.
Type-Safe Primitives
Required accessible names. Enforced ARIA relationships. TypeScript guides you toward accessible patterns.
- →
titleprop required for dialogs - →
labelrequired for icon-only buttons - →ARIA attributes validated at compile time
Runtime Utilities
Focus management, keyboard navigation, and screen reader announcements handled automatically.
- →Focus trap with automatic restoration
- →Roving tabindex for keyboard navigation
- →ARIA live region announcements
Development Validation
Optional runtime checks warn you during development about accessibility violations. Zero cost in production.
- →Accessible name validation
- →Keyboard accessibility checks
- →Tree-shaken in production builds
Framework Agnostic Core
Runtime utilities work with any JavaScript framework or vanilla JS. React bindings provided separately.
- →Use with React, Vue, Svelte, or vanilla JS
- →SSR-safe (Next.js, Remix compatible)
- →~8KB minified + gzipped
Before / After
Compare manual accessibility patterns (error-prone, incomplete) with @a13y primitives (enforced, complete).
1// Before: Manual ARIA (error-prone)2function Dialog({ title, children, onClose }) {3 const dialogRef = useRef();4 const previousFocus = useRef();5 6 useEffect(() => {7 previousFocus.current = document.activeElement;8 dialogRef.current?.focus();9 10 const handleKeyDown = (e) => {11 if (e.key === 'Escape') onClose();12 };13 document.addEventListener('keydown', handleKeyDown);14 15 return () => {16 previousFocus.current?.focus();17 document.removeEventListener('keydown', handleKeyDown);18 };19 }, []);20 21 return (22 <div ref={dialogRef} role="dialog" tabIndex={-1}>23 <h2>{title}</h2>24 {children}25 </div>26 );27}28 29// Missing:30// - Focus trap (Tab exits dialog)31// - aria-labelledby connection32// - Click outside handling33// - Body scroll lock34// - Focus restoration fails if element unmounted1// After: @a13y/react2import { AccessibleDialog } from '@a13y/react';3 4function Dialog({ title, children, isOpen, onClose }) {5 return (6 <AccessibleDialog7 isOpen={isOpen}8 onClose={onClose}9 title={title} // Required by type system10 >11 {children}12 </AccessibleDialog>13 );14}15 16// Includes automatically:17// ✓ Focus trap with Tab/Shift+Tab cycling18// ✓ Focus restoration (handles unmounted elements)19// ✓ Escape key handling20// ✓ Click-outside to close21// ✓ Proper aria-labelledby / aria-describedby22// ✓ Body scroll lock23// ✓ Development-time validationTry It Live
Interact with components built using @a13y/react. Try keyboard navigation (Tab, Escape, Enter) and screen reader compatibility.
AccessibleDialog
Click the button below to open an accessible dialog. Notice the focus trap, keyboard handling (Escape to close), and focus restoration.
What @a13y enforces:
- ✓ Required
titleprop - ✓ Focus trap (Tab cycles within dialog)
- ✓ Escape key closes dialog
- ✓ Focus restoration on close
- ✓ Click backdrop to close
Screen Reader Announcer
Increment the counter. Changes are announced to screen readers via ARIA live regions managed by @a13y/core.
What @a13y provides:
- ✓
announce()utility - ✓ Polite/assertive live regions
- ✓ Announcement queueing
- ✓ Automatic cleanup
Test with Assistive Technology
Try these demos with a screen reader to experience the accessibility features:
- Windows:
NVDA (free) or JAWS - macOS:
VoiceOver (Cmd+F5) - Mobile:
TalkBack / VoiceOver
Architecture
@a13y is a monorepo with three packages. Use them independently or together.
@a13y/core
~8KB gzippedFramework-agnostic runtime utilities. Use with React, Vue, Svelte, or vanilla JavaScript.
Modules:
- runtime/focus
- runtime/keyboard
- runtime/announce
- runtime/aria
Features:
- Focus management & trap
- Keyboard navigation
- Live region announcements
- ARIA utilities
@a13y/react
~12KB gzippedReact hooks and components built on @a13y/core. Includes type-safe APIs and required accessibility props.
Hooks:
- useAccessibleButton
- useAccessibleDialog
- useFocusTrap
- useKeyboardNavigation
Components:
- AccessibleDialog
- AccessibleButton
- AccessibleMenu
- AccessibleTabs
@a13y/devtools
0KB in productionDevelopment-time validators and runtime checks. Automatically tree-shaken in production builds.
Validation:
- Accessible name checks
- Keyboard accessibility
- ARIA attribute validation
- Development warnings
Usage:
- Install as devDependency
- Import conditionally
- Zero runtime cost
- Optional peer dependency
Dependency Tree:
@a13y/core (no dependencies, framework-agnostic) │ ├─→ @a13y/react (depends on core + React 18+) │ └─→ @a13y/devtools (peer dependency on core, optional)
Get Started
Install via npm or pnpm. Start using type-safe accessible components in minutes.
1. Install
# Install core utilities (framework-agnostic)npm install @a13y/core # Install React hooks and componentsnpm install @a13y/react # Install devtools (optional, development only)npm install -D @a13y/devtoolsRequirements:
- • Node.js >= 18.0.0
- • TypeScript >= 5.0
- • React >= 18.0 (for @a13y/react)
2. Use in Your App
1import { AccessibleDialog } from '@a13y/react';2import { announce } from '@a13y/core/runtime/announce';3import { useState } from 'react';4 5function App() {6 const [isOpen, setIsOpen] = useState(false);7 8 const handleSave = () => {9 // Announce to screen readers10 announce('Settings saved successfully');11 setIsOpen(false);12 };13 14 return (15 <>16 <button onClick={() => setIsOpen(true)}>17 Open Settings18 </button>19 20 <AccessibleDialog21 isOpen={isOpen}22 onClose={() => setIsOpen(false)}23 title="Settings" // Required by type system24 >25 <p>Update your preferences here.</p>26 <button onClick={handleSave}>Save</button>27 </AccessibleDialog>28 </>29 );30}Philosophy
Why @a13y exists and what it's designed to do (and not do).
Why Open Source
Accessibility is not a competitive advantage. It's a baseline requirement for usable software.
Making @a13y open source means more developers can build accessible interfaces without reinventing focus management, keyboard navigation, and ARIA patterns.
License: MIT. Use it anywhere, commercially or personally.
Who This Is For
- →Frontend developers building React applications who want to enforce accessibility without becoming WCAG experts
- →Teams shipping production code that needs to work for everyone, including keyboard and screen reader users
- →Library authors who want framework-agnostic accessibility utilities without heavy dependencies
What @a13y Does NOT Do
✗ Not a Testing Tool
@a13y does not replace axe-core, Lighthouse, or manual testing. Use those tools for comprehensive audits.
✗ Not a Component Library
@a13y provides headless primitives and utilities, not styled UI components. Bring your own design system.
✗ Not WCAG Certification
@a13y reduces mechanical errors but doesn't guarantee compliance. You can still write accessible-but-confusing UIs.
✗ Not for Legacy Browsers
Requires modern JavaScript (ES2020+). No Internet Explorer 11 support.
Core Principle
"Accessibility violations are bugs, not features to add later. By encoding constraints in the type system and runtime, we make inaccessible code harder to write and easier to catch during development."
This is not about perfect WCAG scores. This is about building interfaces that work for everyone by default, enforced through tools developers already use.
Contributing
@a13y is in early development (v0.x). APIs may change. Contributions welcome once APIs stabilize.
Report Issues or Feedback