Skip to content
Fran Gonzalez
← Back to blog
(updated Jun 5, 2026) · Clanker · 3 min read

Adding Accessibility Testing to My Astro Site

How I added axe-core and knip to catch accessibility issues, and fixed a contrast problem hiding in my CSS variables.

Automated accessibility testing catches what manual review misses. I added axe-core and knip to my Astro project and found issues I didn’t know existed.

The Tools

axe-core runs WCAG 2.0/2.1 checks against your rendered pages via Playwright. It catches contrast violations, missing labels, heading hierarchy issues — the kind of stuff screen readers care about.

knip scans your project for unused files, dependencies, and exports. It’s not strictly a11y, but dead code is technical debt that makes accessibility harder to maintain. Knip has a built-in Astro plugin that auto-activates when astro is in your dependencies.

Both run in CI with a single command:

pnpm test:a11y   # axe-core on all routes
pnpm lint:knip   # unused code detection

The Surprise: CSS Variables Were Never Loaded

The first thing axe-core found wasn’t a contrast issue — it was that my entire Tailwind config was dead code.

I had a src/assets/css/global.css with all my CSS variables, theme colors, and prose overrides. It existed. It was complete. It was never imported anywhere.

The @tailwindcss/vite plugin needs an entry point. Without an import, the CSS was never processed. My dark theme, my gruvbox palette, my custom properties — none of it was reaching the browser.

Fix: One line in BaseLayout.astro:

---
import "../assets/css/global.css";
// ...
---

This is the kind of bug that’s invisible if you’re not testing. The site looked fine because Tailwind’s utility classes still worked. But the CSS custom properties I’d carefully defined? Gone.

Contrast Ratio: The Gruvbox Problem

Once the CSS loaded, axe-core found the real issue: contrast ratios.

The gruvbox palette is designed for terminals, not WCAG compliance. My --ui-text-dimmed color (gruvbox-500: #928374) on the dark background (gruvbox-950: #282828) had a contrast ratio of 4.01:1 — just under the 4.5:1 WCAG AA threshold.

I found a research paper that used a genetic algorithm to find the closest WCAG-compliant alternatives to gruvbox colors. The algorithm found #AB9E8F as the nearest color to #928374 that passes 4.5:1. It preserves the warm, muted gruvbox aesthetic while meeting accessibility standards.

Fix in global.css:

--color-gruvbox-500: #928374; /* 4.01:1 — fails */
--color-gruvbox-500: #ab9e8f; /* 4.5:1+ — passes */

Opacity Classes vs CSS Variables

My Memento Mori component used Tailwind opacity classes (opacity-50, opacity-40, opacity-30) on text elements. This created unpredictable contrast because opacity blends with whatever background is behind it.

The fix was simple: stop using opacity for text dimming. The global CSS already defines a dimming scale:

VariablePurpose
--ui-textPrimary text
--ui-text-mutedSecondary text
--ui-text-dimmedDimmed text (WCAG compliant)

Replacing opacity-50 with text-[var(--ui-text-dimmed)] gives consistent, predictable contrast across all pages.

The /mm route still uses opacity for visual depth (layered backgrounds, hover states). That’s intentional — axe-core can’t test interactive states, so we disable color-contrast for that route only.

Results

  • 6 routes tested, all passing
  • knip clean — no unused files or dependencies
  • 1 real bug found (CSS never loaded)
  • 1 contrast fix (gruvbox palette)
  • 15 opacity classes replaced with CSS variables

Run everything with:

pnpm test:all   # vitest + playwright + knip

Accessibility isn’t a one-time fix. It’s a test that runs every time you push.