Skip to main content

Accessibility
as a Constraint

Not a best practice. Not a linter rule. A constraint enforced by TypeScript types and runtime utilities that make inaccessible code harder to write.

typescript
import { AccessibleDialog } from '@a13y/react';
function App() {
return (
<AccessibleDialog
isOpen={isOpen}
onClose={() => setIsOpen(false)}
title="Delete confirmation" // Required by type system
>
<p>This action cannot be undone.</p>
<button onClick={handleConfirm}>Confirm</button>
</AccessibleDialog>
);
}

Required accessible name. Focus trap. Keyboard handling. All enforced.

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

typescript
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

typescript
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

typescript
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.

  • title prop required for dialogs
  • label required 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).

BeforeManual implementation
typescript
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 connection
32// - Click outside handling
33// - Body scroll lock
34// - Focus restoration fails if element unmounted
AfterWith @a13y/react
typescript
1// After: @a13y/react
2import { AccessibleDialog } from '@a13y/react';
3
4function Dialog({ title, children, isOpen, onClose }) {
5 return (
6 <AccessibleDialog
7 isOpen={isOpen}
8 onClose={onClose}
9 title={title} // Required by type system
10 >
11 {children}
12 </AccessibleDialog>
13 );
14}
15
16// Includes automatically:
17// ✓ Focus trap with Tab/Shift+Tab cycling
18// ✓ Focus restoration (handles unmounted elements)
19// ✓ Escape key handling
20// ✓ Click-outside to close
21// ✓ Proper aria-labelledby / aria-describedby
22// ✓ Body scroll lock
23// ✓ Development-time validation

Try 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 title prop
  • ✓ 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.

0

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 gzipped

Framework-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 gzipped

React 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 production

Development-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

bash
# Install core utilities (framework-agnostic)
npm install @a13y/core
# Install React hooks and components
npm install @a13y/react
# Install devtools (optional, development only)
npm install -D @a13y/devtools

Requirements:

  • • Node.js >= 18.0.0
  • • TypeScript >= 5.0
  • • React >= 18.0 (for @a13y/react)

2. Use in Your App

typescript
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 readers
10 announce('Settings saved successfully');
11 setIsOpen(false);
12 };
13
14 return (
15 <>
16 <button onClick={() => setIsOpen(true)}>
17 Open Settings
18 </button>
19
20 <AccessibleDialog
21 isOpen={isOpen}
22 onClose={() => setIsOpen(false)}
23 title="Settings" // Required by type system
24 >
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