Loading...
Loading...
> WCAG 2.2 AA compliance guidelines and accessibility patterns for Fabrk components.
Fabrk targets WCAG 2.2 Level AA compliance. All components are built with accessibility as a core requirement, not an afterthought.
Fabrk uses OKLCH color format for perceptually uniform contrast. All theme tokens are pre-validated for WCAG 2.2 compliance.
/* WCAG 2.2 AA Requirements */ Text (normal): 4.5:1 minimum Text (large): 3:1 minimum UI Components: 3:1 minimum (borders, icons, controls) Focus indicators: 3:1 minimum against adjacent colors /* Example OKLCH token with sufficient contrast */ --foreground: oklch(98% 0.01 145); /* Light text */ --background: oklch(15% 0.02 145); /* Dark bg, ~12:1 ratio */
Test your customizations with these tools:
All Fabrk components support full keyboard navigation. Focus rings are always visible for keyboard users.
/* Focus ring styles (applied globally) */
:focus-visible {
outline: 2px solid var(--ring);
outline-offset: 2px;
}
/* Never hide focus for keyboard users */
.focus-visible:focus-visible {
outline: 2px solid var(--ring);
}[TAB] Navigation
Move focus between interactive elements. Tab order follows DOM order.
[ARROW] Keys
Navigate within components (tabs, menus, radio groups, sliders).
[ENTER/SPACE]
Activate buttons, links, and toggles. Space scrolls when not on interactive element.
[ESCAPE]
Close dialogs, sheets, popovers, and dropdown menus.
Fabrk components use semantic HTML first, ARIA attributes only when necessary.
/* Icon-only button - REQUIRES aria-label */
<Button aria-label="Close dialog" size="icon">
<X className="h-4 w-4" />
</Button>
/* Decorative icons - hide from screen readers */
<Icon aria-hidden="true" />
/* Live regions for dynamic updates */
<div role="status" aria-live="polite">
{statusMessage}
</div>
/* Dialog with proper labeling */
<Dialog>
<DialogContent aria-labelledby="dialog-title" aria-describedby="dialog-desc">
<DialogTitle id="dialog-title">Confirm Action</DialogTitle>
<DialogDescription id="dialog-desc">Are you sure?</DialogDescription>
</DialogContent>
</Dialog>Fabrk respects the prefers-reduced-motion media query. Users who prefer reduced motion see minimal or no animations.
/* CSS approach */
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}
/* Framer Motion approach (built-in) */
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
/* Framer Motion automatically respects reduced motion */
/>/* Every input needs an associated label */
<div className="space-y-2">
<Label htmlFor="email">Email address</Label>
<Input id="email" type="email" aria-describedby="email-error" />
{error && (
<p id="email-error" role="alert" className="text-destructive text-xs">
[ERROR]: {error}
</p>
)}
</div>
/* Required fields */
<Label htmlFor="name">
Name <span aria-hidden="true">*</span>
<span className="sr-only">(required)</span>
</Label>
<Input id="name" required aria-required="true" />
/* Group related inputs */
<fieldset>
<legend className="text-sm font-medium">Notification Preferences</legend>
<RadioGroup name="notifications" aria-label="Notification preferences">
<RadioGroupItem value="email" id="notify-email" />
<Label htmlFor="notify-email">Email</Label>
...
</RadioGroup>
</fieldset># Run accessibility audit with axe-core
npx playwright test --project=a11y
# Or use Lighthouse in Chrome DevTools
# Run accessibility audit category
# ESLint jsx-a11y rules (built into Fabrk)
npm run lint
# axe-core programmatic testing
import { axe, toHaveNoViolations } from 'jest-axe';
expect.extend(toHaveNoViolations);
test('page has no accessibility violations', async () => {
const { container } = render(<Page />);
const results = await axe(container);
expect(results).toHaveNoViolations();
});/* ALWAYS add aria-label to icon-only buttons */ // CORRECT <Button aria-label="Delete item" variant="ghost" size="icon"> <Trash className="h-4 w-4" /> </Button> // WRONG - screen reader announces nothing useful <Button variant="ghost" size="icon"> <Trash className="h-4 w-4" /> </Button>
/* Announce loading state to screen readers */
<Button disabled={isLoading} aria-busy={isLoading}>
{isLoading ? (
<>
<TerminalSpinner size="sm" aria-hidden="true" />
<span className="sr-only">Loading...</span>
</>
) : (
'> SUBMIT'
)}
</Button>
/* For page-level loading */
<div role="status" aria-live="polite" aria-busy={isLoading}>
{isLoading ? 'Loading content...' : children}
</div>/* Provide skip link for keyboard users */
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:z-50
focus:bg-background focus:p-4 focus:text-foreground"
>
Skip to main content
</a>
<main id="main-content" tabIndex={-1}>
{/* Page content */}
</main>What to check: All interactive elements announce their purpose, form labels are read, error messages announce, loading states communicate.
/* Polite - waits for user to finish current task */
<div aria-live="polite" aria-atomic="true">
{notificationCount} new notifications
</div>
/* Assertive - interrupts immediately (use sparingly) */
<div role="alert" aria-live="assertive">
[ERROR]: Session expired. Please log in again.
</div>
/* Status - implicit polite, for status messages */
<div role="status">
Saving changes...
</div>
/* Log - for chat/feed updates */
<div role="log" aria-live="polite">
{messages.map(m => <p key={m.id}>{m.text}</p>)}
</div>
/* Timer - countdown or elapsed time */
<div role="timer" aria-live="off" aria-atomic="true">
{formatTime(remainingSeconds)}
</div>/* PREFER: Native HTML elements (built-in accessibility) */
<button onClick={handleClick}>Click me</button>
<a href="/page">Link text</a>
<input type="checkbox" />
<select><option>Choice</option></select>
/* AVOID: Div with ARIA (requires more work) */
<div role="button" tabIndex={0} onClick={handleClick}
onKeyDown={handleKeyDown}>Click me</div>
/* Native elements provide FREE: */
// - Keyboard activation (Enter/Space)
// - Focus management
// - Screen reader announcements
// - Form submission behavior
// - Proper semanticsUse ARIA only when native HTML can't provide the needed semantics:
Rule of thumb: If a native HTML element exists, use it. ARIA can override but never add behavior - you must implement keyboard handling yourself.