Web Accessibility & Interface Guidelines
Overview
Enforces universal accessibility compliance (WCAG 2.1 AA / AAA), rigorous semantic markup, keyboard navigability with focus traps, screen reader live regions, and Web Interface Guidelines standards.
When to Use
Activate whenever building, styling, or reviewing user interfaces, forms, modals, menus, navigation drawers, custom interactive widgets, or media elements.
Negative Constraints (What NOT to Do)
- NEVER use
outline: nonewithout a custom:focus-visiblereplacement: Keyboard users must always have a distinct, high-contrast visual focus ring. - NEVER use non-semantic elements (
<div onClick>) for interactive triggers: Always use native<button>or<a href>. - NEVER create modals or dialogs without keyboard focus traps: Focus must remain trapped inside open dialogs during Tab / Shift-Tab navigation and restore to trigger on close.
- NEVER rely exclusively on color to indicate state or errors: Always pair colors with text labels, icons, or ARIA attributes (
aria-invalid="true"). - NEVER trap screen readers with missing form labels or error associations: Every input must link to
<label htmlFor="id">and errors viaaria-describedby. - NEVER play animations without honoring
prefers-reduced-motion: Respect user OS motion reduction preferences.
Rules & Patterns
1. Focus Visible & High-Contrast Focus Rings
button:focus-visible,
a:focus-visible,
input:focus-visible {
outline: 2px solid #6366f1;
outline-offset: 2px;
border-radius: 4px;
}
button:focus:not(:focus-visible) {
outline: none;
}
2. Accessible Modal & Focus Trap Contract
import React, { useEffect, useRef } from 'react';
import { createPortal } from 'react-dom';
interface ModalProps {
isOpen: boolean;
onClose: () => void;
titleId: string;
children: React.ReactNode;
}
export function AccessibleModal({ isOpen, onClose, titleId, children }: ModalProps) {
const dialogRef = useRef<HTMLDivElement>(null);
const triggerRef = useRef<HTMLElement | null>(null);
useEffect(() => {
if (!isOpen) return;
triggerRef.current = document.activeElement as HTMLElement;
const handleKeyDown = (e: KeyboardEvent) => {
if (e.key === 'Escape') {
e.preventDefault();
onClose();
}
if (e.key === 'Tab') {
const focusables = dialogRef.current?.querySelectorAll<HTMLElement>(
'button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])'
) || [];
if (!focusables.length) return;
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
};
document.addEventListener('keydown', handleKeyDown);
return () => {
document.removeEventListener('keydown', handleKeyDown);
triggerRef.current?.focus();
};
}, [isOpen, onClose]);
if (!isOpen) return null;
return createPortal(
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/60 p-4">
<div
ref={dialogRef}
role="dialog"
aria-modal="true"
aria-labelledby={titleId}
className="w-full max-w-lg rounded-xl bg-background p-6 shadow-2xl border"
>
{children}
</div>
</div>,
document.body
);
}
3. Accessible Forms & Error Association
export function EmailInput({ error, ...props }: { error?: string } & React.InputHTMLAttributes<HTMLInputElement>) {
const inputId = 'user-email';
const errorId = 'user-email-error';
return (
<div className="flex flex-col gap-1.5">
<label htmlFor={inputId} className="text-sm font-medium">
Email Address <span aria-hidden="true" className="text-destructive">*</span>
</label>
<input
id={inputId}
type="email"
autoComplete="email"
required
aria-invalid={Boolean(error)}
aria-describedby={error ? errorId : undefined}
className="rounded-md border p-2 text-sm focus-visible:ring-2"
{...props}
/>
{error && (
<p id={errorId} role="alert" className="text-xs text-destructive">
{error}
</p>
)}
</div>
);
}
Code Examples
See EXAMPLES.md for detailed dialog, menu, and form examples.
Validation Checklist
- All interactive elements are fully operable via Keyboard (
Tab,Enter,Space,Escape). - Visual
:focus-visiblestyling is distinct and high contrast (≥ 3:1). - Modals use
role="dialog",aria-modal="true", focus trap, and restore focus on close. - Text contrast ratios satisfy WCAG AA (≥ 4.5:1 for normal text, ≥ 3:1 for large text).
- Forms pair inputs with
<label htmlFor>, validautocompletetokens, andaria-invalid. - Non-text media contains descriptive
altattributes oraria-hidden="true"for decorative icons.
Common Mistakes
- Hiding outline focus indicators globally without
:focus-visiblefallback. - Forgetting to trap focus in modal dialogs or not returning focus to trigger when modal closes.
- Missing
aria-expandedattributes on disclosure buttons and dropdown toggles.
Integration Notes
- Pairs with
ui-ux-proandimpeccable-designfor visual contrast and component standards. - Pairs with
reactandnextjsfor accessible dialogs and focus restoration across route transitions.
Web Accessibility — WCAG 2.1 Compliance
Principles (POUR)
- Perceivable — content can be perceived by all users
- Operable — interface can be operated by all users
- Understandable — content and interface are understandable
- Robust — content works across assistive technologies
Keyboard Navigation
- All interactive elements must be reachable with Tab
- Focus order must be logical (DOM order)
- Custom widgets need keyboard handlers (Enter, Space, Escape, Arrow keys)
- Visible focus indicator on all interactive elements (never
outline: nonewithout replacement) - Skip navigation link for repeated content
Semantic HTML
- Use
<button>not<div onClick>for actions - Use
<a href>for navigation - Use heading hierarchy (
<h1>→<h2>→<h3>) - Use
<nav>,<main>,<article>,<aside>landmarks - Use
<label>with every form input
ARIA (when HTML alone isn't enough)
aria-label— label for screen readers when no visible textaria-labelledby— reference to existing visible textaria-describedby— additional descriptionaria-live="polite"— announce dynamic changesaria-expanded— for collapsible sectionsaria-hidden="true"— hide decorative elements
Rule: no ARIA is better than bad ARIA. Use semantic HTML first.
Color and Contrast
- Text contrast: 4.5:1 minimum (AA), 7:1 (AAA)
- Large text (18px+ bold, 24px+ regular): 3:1 minimum
- Never use color alone to convey information
- Test with grayscale filter
Images
- All images need
alttext - Decorative images:
alt="" - Complex images:
aria-describedbywith longer description - SVG icons:
role="img"+aria-label
Forms
- Every input needs a visible
<label> - Error messages linked with
aria-describedby - Required fields marked with
aria-required="true" - Group related fields with
<fieldset>+<legend>
Testing
- Automated: axe-core, Lighthouse accessibility audit
- Manual: keyboard-only navigation test
- Screen reader: test with NVDA (Windows), VoiceOver (Mac)
- Zoom: test at 200% and 400% zoom
web-accessibility Examples — Anti-patterns vs ContextOS Standard
Example 1: Semantic Buttons vs Clickable Divs
Anti-pattern: Clickable Div
// BAD: Cannot be focused with Tab, does not respond to Enter or Space, silent to screen readers
<div className="button"
Best practice: ContextOS Standard (Semantic Button Element)
// GOOD: Keyboard focusable, native Enter/Space handling, properly announced by assistive tech
<button type="button" className="btn btn-primary">
Submit
</button>
Example 2: Icon-only Buttons
Anti-pattern: Unlabelled Icon Button
// BAD: Screen reader announces "button", user has zero idea what it does
<button /></button>
Best practice: ContextOS Standard (Accessible Label)
// GOOD: Explicit aria-label and hidden decorative icon
<button type="button" aria-label="Close modal window">
<XIcon aria-hidden="true" />
</button>
web-accessibility Troubleshooting & Common Mistakes
1. Trapping Keyboard Users in Inactive Elements
- Symptom: Tab key moves focus into invisible elements hidden offscreen.
- Root Cause: Using display: none vs opacity: 0 or left: -9999px.
- Fix: Always apply display: none / hidden or inert attribute to elements that are currently not visible.
2. Color Contrast Violations
- Symptom: Text is unreadable for users with low vision or in bright sunlight.
- Root Cause: Contrast ratio between text and background color is below WCAG AA thresholds.
- Fix: Ensure contrast ratio is at least 4.5:1 for body text and 3:1 for large text / graphical controls.
3. Silent Dynamic Updates
- Symptom: Asynchronous error messages or notifications appear on screen without screen reader announcement.
- Root Cause: Missing ARIA live region.
- Fix: Wrap notification banners in aria-live="polite" and role="status".