Accessibility

Last updated on 2026-03-22

The Blog CMS Kit is built with WCAG AA accessibility as a baseline. Every component and page meets or exceeds these standards, using semantic HTML, ARIA attributes, and accessible interaction patterns throughout all 25 screens.

Every page includes a skip-to-content link as the first focusable element:

import { SkipLink } from "@/components/layout/skip-link"

<SkipLink />

The skip link is visually hidden until focused. It jumps the user directly to the #main-content landmark, bypassing the blog header or admin sidebar navigation.

Color Contrast

All text meets WCAG AA contrast ratios:

  • Normal text (< 18px): minimum 4.5:1 contrast ratio
  • Large text (>= 18px bold or >= 24px): minimum 3:1 contrast ratio
  • UI components and states: minimum 3:1 against adjacent colors

The oklch color token system ensures contrast is maintained in both light and dark mode. The amber/orange theme (hue 55) was specifically tuned so that primary-on-background and foreground-on-muted combinations pass AA thresholds.

Keyboard Navigation

All interactive elements are keyboard accessible:

  • Tab -- navigate between focusable elements in logical order
  • Shift+Tab -- navigate backwards
  • Enter/Space -- activate buttons, links, and toggles
  • Arrow keys -- navigate within menus, radio groups, and tabs
  • Escape -- close dialogs, sheets, and popovers

Focus Indicators

Visible focus rings appear on all interactive elements using the --ring design token:

:focus-visible {
  outline: 2px solid var(--ring);
  outline-offset: 2px;
}

Focus Traps

Focus is trapped inside overlay components to prevent keyboard users from tabbing behind them:

  • Dialog -- post delete confirmation, media upload dialog
  • Sheet -- mobile sidebar navigation, mobile blog menu
  • Command -- search command palette

When these components close, focus returns to the trigger element that opened them.

Screen Reader Support

Semantic HTML

All pages use proper HTML5 landmarks:

  • <header> for blog header and admin header
  • <nav> with aria-label for main navigation, admin navigation, and mobile navigation
  • <main> for primary content area
  • <article> for blog posts and post cards
  • <footer> for blog footer
  • <time> with datetime attribute for publish dates
  • <aside> for the admin sidebar

ARIA Labels

Icon-only buttons include descriptive labels:

<Button aria-label="Search posts">
  <Search className="size-4" />
</Button>

<Button aria-label="Switch to dark mode">
  <Moon className="size-5" />
</Button>

Live Regions

The LiveRegion component announces dynamic content changes to screen readers:

import { LiveRegion } from "@/components/a11y/live-region"

<LiveRegion message="Post published successfully" politeness="polite" />

Heading Hierarchy

Every page follows a strict heading hierarchy:

  • One <h1> per page (the page title)
  • <h2> for major sections
  • <h3> for subsections within those sections
  • No skipped heading levels

For example, an article page uses <h1> for the post title, <h2> for the "Comments" and "Related Posts" sections, and <h3> within the article body as needed.

Touch Targets

All interactive elements meet the 44x44px minimum touch target size. The kit includes a .touch-target utility class:

.touch-target {
  min-width: 44px;
  min-height: 44px;
}

This class is applied to icon buttons in the blog header (search, RSS, theme toggle, menu), admin header controls, and navigation links on mobile.

Reduced Motion

The kit respects the prefers-reduced-motion media query. When enabled:

@media (prefers-reduced-motion: reduce) {
  *,
  *::before,
  *::after {
    animation-duration: 0.01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: 0.01ms !important;
    scroll-behavior: auto !important;
  }
}

This disables all animations including fade-in-up effects, the reading progress bar transition, float animations, shimmer loading states, button press micro-interactions, and hover transforms on post cards.

Status Indicators

Status information is never conveyed by color alone. The kit uses color plus icon plus text:

{/* Post status uses icon + text + color */}
<TrendingUp className="size-3 text-chart-4" />
<span className="text-chart-4">+12%</span>

{/* Destructive actions use icon + text + color */}
<TrendingDown className="size-3 text-destructive" />
<span className="text-destructive">-5%</span>

The StatusBadge component in the admin panel combines a color-coded badge with readable text labels (Published, Draft, Scheduled, Archived).

Form Accessibility

Forms across the kit follow accessibility best practices:

  • Labels associated with inputs via htmlFor/id
  • Error messages linked with aria-describedby
  • Required fields marked with visual indicators and aria-required
  • Invalid fields marked with aria-invalid for screen reader announcement
  • The newsletter subscription form and admin settings forms all follow these patterns

Testing Recommendations

Tool Purpose
axe DevTools Automated accessibility scanning
VoiceOver (macOS) Screen reader testing
NVDA (Windows) Screen reader testing
Keyboard-only navigation Tab through every page
Lighthouse Accessibility audit score