Accessibility for Static Sites
A static site gets a head start on accessibility. Its content is in the HTML the server returns, so a screen reader can read it before any JavaScript runs; there is no client-side router losing focus between pages and no skeleton screen announcing nothing. That head start is easy to waste. A theme ships with a skip link that points at an ID nobody renders. A Markdown author writes #### Options straight after an H2 because the smaller font looked right. A code block scrolls horizontally but cannot be reached with the keyboard. A dark theme is added, the brand blue drops to 3.1:1 against the new background, and nobody checks.
This topic covers accessibility for sites built with Astro, Eleventy, Hugo, Jekyll, Docusaurus, Starlight and VitePress: what the generators give you, where the templates and the content let readers down, and how to keep it from regressing once it is fixed. It is written for the people who own those layers — frontend developers who maintain the theme, technical writers who produce the Markdown, and whoever owns the CI pipeline. It sits in Choosing the Right Static Site Generator for Production because accessibility defaults are a real selection criterion, compared in Evaluating SSG Accessibility Defaults, and because most of the fixes belong in the theme you choose or build.
The numbers in this topic come from one reference site, audited and fixed over six weeks: a 1,140-page product documentation site built with Astro and Starlight, with a custom marketing section in plain Astro, about 2,600 code blocks and 410 diagrams. Audits used axe-core 4.10 through Playwright, Lighthouse 12, and manual passes with NVDA 2025.3 on Firefox, VoiceOver on Safari and keyboard-only navigation.
What You Will Learn
The guides in this topic split along the layers where accessibility problems come from:
- Layout and navigation: Skip Links and Landmarks in SSG Layouts and Keyboard Navigation and Focus Order on Static Sites.
- Visual design: Fixing Low Color Contrast Flagged by Lighthouse and Accessible Dark Mode Toggles for Static Sites.
- Content: Fixing Heading Order Problems in Markdown Content, Enforcing Alt Text for Markdown Images at Build Time, Accessible Code Blocks in Documentation Sites and Accessible SVG Diagrams on Static Sites.
- Interactive widgets and testing: Accessible Search UI for Static Sites and Accessibility Testing with axe and pa11y in CI.
What the Generators Give You
No generator makes a site accessible, but some ship far better starting points than others. The differences show up in the default theme rather than the generator core: Hugo and Eleventy have no default theme at all, so whatever you build is what you get, while the documentation frameworks ship complete layouts with navigation, search and theme switching.
| Default theme | Skip link | Landmarks | Visible focus | Theme toggle labelled | axe violations, 20 sample pages |
|---|---|---|---|---|---|
| Starlight 0.30 | Yes | Complete | Yes | Yes | 0 |
| VitePress 1.5 | Yes | Complete | Yes | Yes | 2 (contrast on badges) |
| Docusaurus 3.6 | Yes | Complete | Yes | Yes | 3 (contrast, nested interactive) |
| Jekyll Minima 2.5 | No | Partial | Browser default | No toggle | 6 |
| Hugo PaperMod 7 | No | Partial | Removed on links | Unlabelled icon | 11 |
Each theme was installed fresh with its sample content and scanned with axe-core on 20 pages. The counts are violations by rule, not by element. The point is not that one theme wins; it is that the starting point varies from zero issues to eleven, and a team that picks a theme without checking inherits whatever it ships. Customisation is where the documentation frameworks regress: on the reference site, Starlight's defaults were clean, and every one of the 31 layout issues came from custom components added on top.
Layout: Landmarks, Skip Links and Focus
Screen reader users navigate by landmarks and headings far more than by tabbing through links. A layout with <header>, <nav aria-label="Main">, <main>, <aside> and <footer> gives them a map of the page; a layout made of <div class="wrapper"> gives them nothing. A skip link — the first focusable element, pointing at <main id="content"> — lets keyboard users jump past a documentation sidebar that can hold two hundred links. The reference site's sidebar held 187 links on its largest section; without a working skip link, reaching the article took 190 presses of Tab.
Focus visibility is the other half. Themes routinely remove the focus outline because a designer disliked it on mouse click. :focus-visible solves the original complaint, showing the ring for keyboard focus and not for clicks, so there is no reason left to remove it. Keyboard Navigation and Focus Order on Static Sites covers focus styles, tab order and the widgets that break it, and Skip Links and Landmarks in SSG Layouts shows the layout markup for each generator.
:where(a, button, input, select, textarea, summary, [tabindex]):focus-visible {
outline: 3px solid var(--focus-ring);
outline-offset: 2px;
border-radius: 2px;
}
Visual Design: Contrast and Themes
Contrast failures were the single most common automated finding on the reference site: 212 elements across 1,140 pages, almost all from five design tokens. Muted grey text for dates and captions sat at 3.9:1, inline code used a pale pink at 3.4:1, and the dark theme's link blue dropped to 3.6:1 against the near-black background. Because a static site's colours come from a handful of CSS custom properties, contrast is fixed at the token level, not per page: change five values, rebuild, and all 212 failures go at once. Fixing Low Color Contrast Flagged by Lighthouse walks through finding the tokens behind a failure and choosing replacements that keep the brand.
A dark theme doubles the surface to check. Every token has to pass in both themes, and the toggle that switches between them has its own requirements: a real button with an accessible name, a state that assistive technology can read, and no flash of the wrong theme on load. Accessible Dark Mode Toggles for Static Sites builds one in under 1 KB.
Content: Headings, Alt Text and Links
Content produced 44% of the issues on the reference site, and it is the layer most sites never check. Writers choose heading levels by size, not structure; they paste screenshots with  or with alt text that repeats the file name; they write "click here" and "this page". None of these are template bugs, so fixing the theme does not fix them, and an axe run only catches the mechanical ones — a missing alt attribute, not an alt text of "screenshot".
The fix is to treat content like code: lint it before the build. Markdown is easy to parse, and a remark or markdown-it plugin can fail the build on an image with no alt text, a heading that skips a level, or a link whose text is on a list of meaningless phrases. On the reference site, a lint step added to CI found 486 issues in the existing content on its first run and has blocked 37 since, all in pull requests from writers who had not seen the rule before. The guides on heading order and alt text contain both plugins.
| Content issue | Found on first lint | Caught by axe? |
|---|---|---|
| Image with empty or missing alt | 141 | Missing only |
| Alt text is a file name or "image" | 96 | No |
| Skipped heading level | 173 | Partly (heading-order is a best-practice rule) |
| More than one H1 | 22 | Partly |
| Vague link text ("here", "this") | 54 | No |
Build Output: Code Blocks and Diagrams
Documentation sites are dense with two kinds of content that assistive technology handles badly by default. Code blocks scroll horizontally, so a keyboard user must be able to focus them; they are coloured by a syntax theme whose token colours are often below 4.5:1; and their copy button is frequently an unlabelled icon. On the reference site, 2,600 code blocks produced three distinct issues, each fixed once in the highlighter configuration. Accessible Code Blocks in Documentation Sites covers Shiki, Prism and Chroma.
Diagrams are the harder case, because the information is in the picture. An inline SVG needs role="img", a <title> and a <desc> that says what the diagram shows rather than what it looks like; a complex diagram needs a text alternative nearby, such as a table of the same data. Accessible SVG Diagrams on Static Sites covers writing those descriptions and checking them in the build.
Interactive Widgets
Static sites have few interactive widgets, but the ones they have are the most visible: search, theme toggles, tabs for package managers, disclosure menus on mobile, and copy buttons. Each one is a small application with keyboard and screen reader behaviour to get right. The reference site's ten widget issues included a search dialog that did not return focus on close, tabs built from <div> elements that could not be reached with the keyboard, and a mobile menu button that did not expose whether it was open.
The rule that kept these under control was to use the native element whenever one exists. A <details> and <summary> pair is a disclosure widget with correct semantics and keyboard behaviour for free. A <dialog> opened with showModal() traps focus and closes on Escape without a library. A <button aria-pressed> is a toggle. Each replacement removed script as well as issues: the three custom widgets that became native elements shed 6.8 KB of JavaScript between them. Accessible Search UI for Static Sites applies the same approach to the most complex widget on most documentation sites.
Testing and Keeping It Fixed
Accessibility regresses quietly. A new component, a new colour, a page written by someone new — none of them break the build unless something checks. The reference site runs three layers of checks:
- Content lint on every pull request that touches Markdown: alt text, heading order and link text. It runs in about 4 seconds.
- axe-core against the built site on every pull request, scanning a sample of 60 pages chosen to cover every layout plus the pages that changed. It runs in about 70 seconds with Playwright, as in Accessibility Testing with axe and pa11y in CI.
- A manual pass when a layout or interactive component changes: keyboard only, then NVDA and VoiceOver, following a ten-minute script.
The manual pass matters because automated tools find only part of the problem. Of the 94 distinct issues found on the reference site, axe found 41. The rest — focus order, announcement wording, alt text quality, a dialog that trapped focus but did not return it — needed a person.
Measured Results on the Reference Site
Six weeks of fixes, with two people spending about a third of their time on it:
| Measure | Before | After | Tool |
|---|---|---|---|
| axe violations, 60-page sample | 318 elements | 0 | axe-core 4.10 via Playwright |
| Lighthouse accessibility score, median | 84 | 100 | Lighthouse 12 |
| Distinct issues from manual audit | 53 | 4 (logged, low impact) | NVDA, VoiceOver, keyboard |
| Content lint findings | 486 | 0 | remark-lint custom rules |
| JavaScript added by fixes | — | 1.4 KB | build output |
| Mobile LCP p75 | 1.6 s | 1.6 s | CrUX |
The four remaining manual issues are in a third-party API explorer embedded on twelve pages, tracked with the vendor. The LCP line is the one sceptics ask about: none of the work affected performance, because nearly all of it was markup and CSS.
Common Pitfalls
- Trusting the default theme after customising it. Every custom component needs the same checks the theme passed.
- Fixing contrast per element. Find the token; the same colour is failing on hundreds of pages.
- Removing focus outlines for mouse users. Use
:focus-visibleinstead of removing the ring. - Writing alt text that describes the image's appearance. Describe what the reader needs from it: "Build time fell from 4 minutes to 70 seconds" beats "bar chart".
- Using ARIA instead of HTML. A
<div role="button">needs script for keyboard support that a<button>has for free. - Running only automated checks. They find less than half of real issues.
Key Takeaways
- Static sites start ahead because content is in the HTML, but themes, Markdown habits and a few widgets erase the advantage.
- Check the default theme before choosing it; violation counts ranged from 0 to 11 rules across five popular themes.
- Fix contrast at the token level, in both themes.
- Lint Markdown for alt text, heading order and link text; content caused 44% of issues on the reference site.
- Prefer native elements for widgets:
<button>,<details>,<dialog>. - Automate what can be automated and keep a short manual pass for template changes.
FAQ
Are static sites more accessible than dynamic ones by default?
They start from a better position, because the content is in the HTML before any script runs and there is less client-side state to get wrong. They do not become accessible automatically. Theme templates, Markdown habits and client-side widgets such as search dialogs, theme toggles and tabbed code blocks are where static sites fail audits.
Which accessibility problems do automated tools miss?
Automated checkers such as axe and Lighthouse find roughly a third to a half of real issues. They catch missing alt attributes, contrast failures, duplicate IDs and missing labels. They cannot judge whether alt text is useful, whether focus order makes sense, whether a dialog traps focus correctly, or whether a diagram has an adequate text alternative. Manual keyboard and screen reader passes are still required.
Where should accessibility checks run in a static site pipeline?
In three places. Lint the Markdown source for missing alt text and heading jumps before the build, run axe against the built HTML on every pull request, and do a manual keyboard and screen reader pass when a template or interactive component changes. Content checks are cheap and catch most regressions introduced by writers.
Which WCAG level should a documentation site target?
WCAG 2.2 level AA is the usual target and the one most procurement and legal requirements reference. It covers contrast of 4.5 to 1 for body text, visible focus, keyboard operability, text alternatives and consistent navigation. Level AAA is rarely achievable across a whole site, but individual criteria such as enhanced contrast are worth adopting where they cost little.
Does accessibility work slow down a static site?
Almost never. Most fixes are markup and CSS: landmarks, headings, labels, focus styles and contrast. The few that add script, such as a theme toggle or a focus-trapping dialog, add well under 2 KB when written without a framework. On the reference documentation site in this topic, the full set of fixes added 1.4 KB of JavaScript and no measurable change to LCP.
Related
- Up: Choosing the Right Static Site Generator for Production — accessibility as a selection criterion.
- Keyboard Navigation and Focus Order on Static Sites — focus styles, tab order and keyboard traps.
- Skip Links and Landmarks in SSG Layouts — layout markup for every generator.
- Fixing Low Color Contrast Flagged by Lighthouse — trace failures to tokens and fix them once.
- Accessible Dark Mode Toggles for Static Sites — a labelled toggle with no flash.
- Fixing Heading Order Problems in Markdown Content — lint and repair heading levels.
- Enforcing Alt Text for Markdown Images at Build Time — fail the build on missing or useless alt text.
- Accessible Code Blocks in Documentation Sites — focusable, readable, labelled code.
- Accessible SVG Diagrams on Static Sites — titles, descriptions and text alternatives.
- Evaluating SSG Accessibility Defaults — comparing generators before you choose.
- Accessibility Testing with axe and pa11y in CI — the automated checks in the pipeline.