| name | accessibility |
| description | Implement or audit WCAG 2.1 AA accessibility. Use when adding skip navigation, fixing heading hierarchy, setting up aria-live regions, handling reduced motion, adding screen reader text, or auditing a component for a11y compliance. |
Accessibility (WCAG 2.1 AA)
Guidance for accessibility on this site. All pages must meet WCAG 2.1 AA standards.
Skip navigation
Heading hierarchy
- Each page must have exactly one
<h1>.
- Use the
asHeading prop on PageHeader (components/mate/page-header.tsx) to render the page-level <h1>. Do not add a separate <h1> elsewhere on the same page.
- Sections within a page use
<h2>, subsections use <h3>, etc. Never skip levels.
- In MDX articles, the title from frontmatter renders as
<h1>; use ## and ### for sections inside the article body.
Focus management
- Use
focus-visible (not focus) for keyboard-only outline styles in styles/global.css. This prevents the focus ring from showing on mouse clicks.
- Do not suppress focus outlines — keyboard users depend on them.
- For custom interactive regions (e.g., Terminal), use
biome-ignore lint/a11y/... only when there is a documented reason; always maintain keyboard navigation and screen reader behavior. See components/mate/terminal/terminal.tsx and components/ui/carousel.tsx for examples.
Reduced motion
- Global CSS in styles/global.css includes
@media (prefers-reduced-motion: reduce) — CSS animations are disabled at the system level automatically.
- Framer Motion is wrapped with
<MotionConfig reducedMotion="user"> in components/providers/motion-provider.tsx — all motion.* components respect the OS reduced-motion preference automatically.
- Do not add manual
useReducedMotion checks when using Framer Motion components inside MotionProvider — it is already handled.
Live regions for dynamic content
- Wrap dynamic content sections (data that updates after initial render) with
aria-live="polite":
- Spotify player (currently listening, recently played): uses
aria-live="polite" so screen readers announce track changes.
- GitHub recent activity: uses
aria-live="polite" on the activity list container.
- Use
aria-live="assertive" only for truly urgent announcements (errors); prefer "polite" for non-urgent updates.
Screen reader text
Color contrast
- Primary color in light mode uses
amber-600 (not amber-500) to meet 4.5:1 contrast ratio against white backgrounds.
- Dark mode uses
amber-400.
- When adding new color combinations, verify contrast ratio with a tool (e.g., WebAIM Contrast Checker).
biome-ignore a11y exceptions
When a Biome a11y rule must be suppressed for a legitimate interactive component:
- Use
biome-ignore lint/a11y/<rule>: <reason> on the specific line.
- Ensure keyboard navigation still works (Tab, Enter, Escape, arrow keys as appropriate).
- Ensure the component is still usable with a screen reader.
- Prefer fixing the root cause over suppressing the rule.
Verification checklist
Before marking a new page or component as done, verify: