Your Static Site Isn't Accessible by Default

There is a comfortable story static-site people tell themselves. No client-side router hijacking navigation. No framework shipping a div soup of unlabeled buttons. No modal that traps focus because a state library forgot to unmount it. Just semantic HTML, served fast, from the edge.
That story is half true, and the half that is false is the half that matters.
Static rendering removes a whole class of accessibility bugs. It does not produce accessibility. Those are different claims, and conflating them is how you end up with a site that scores 100 and still locks people out.
What static actually buys you
Credit where it is due. Shipping real HTML gets you meaningful defaults:
- Content exists before JavaScript runs, so a screen reader is never waiting on hydration to hear anything.
- Links are
<a href>. They work with middle-click, with keyboard, with a screen reader’s link list. - Document order matches visual order, because there is no client-side reordering.
- No layout thrash from late-arriving components stealing focus mid-interaction.
These are not small. A large fraction of real-world accessibility failures come from interactive frameworks reimplementing native controls badly. Not shipping that code is a genuine win.
But every one of those wins is about what you avoided. None of them is about what you built.
The failures static does not touch
Here is the uncomfortable list. Every item below is fully reachable in a pure-HTML, zero-JavaScript page.
Color contrast. Static rendering has no opinion about whether your body text passes 4.5:1.
Alt text quality. alt="hero image" is valid HTML and communicates nothing. A build step can check that the attribute exists. It cannot check that it means something.
Heading structure. Nothing stops you from jumping ## to #### because the smaller one looked better. Screen-reader users navigate by heading level; a broken outline is a broken table of contents.
Focus visibility. outline: none is one line of CSS and it is still in half the resets on the internet.
Link text. Fifteen links that all say “read more” are fifteen identical entries in a screen reader’s link list.
Motion. A scroll-reveal animation that ignores prefers-reduced-motion can cause genuine nausea for people with vestibular disorders.
Notice what these have in common: they are all authoring decisions. Your renderer cannot make them for you.
Syntax highlighting is the one that gets everybody
If you run a technical blog, this is the failure you almost certainly have right now.
Popular syntax themes were designed to look good, not to pass WCAG. Comments are the usual casualty — they are deliberately de-emphasized, and “de-emphasized” often lands below the contrast floor.
This site had exactly that bug. The github-dark theme renders comments in #6A737D on a #24292e background. That is a contrast ratio of 3.04:1. WCAG AA wants 4.5:1 for normal text. Every code comment on the site was failing, in the one element type a developer audience reads most carefully.
The fix was not to abandon the theme. It was to recolor a single token to GitHub’s lighter muted gray, #8b949e, which lands at 4.77:1 — above the floor, still visually subordinate to the code around it.
shikiConfig: {
theme: 'github-dark',
transformers: [
{
name: 'accessible-comment-contrast',
tokens(lines) {
for (const line of lines) {
for (const token of line) {
if (token.color?.toUpperCase() === '#6A737D') {
token.color = '#8b949e'
}
}
}
},
},
],
}
Because highlighting happens at build time, this costs nothing at runtime. The corrected colors are baked into the HTML. Static architecture did not prevent the bug, but it did make the fix free.
Go check your own theme’s comment color. Bring a contrast checker. This one is nearly universal.
Dark mode doubles your surface area
A palette that passes in light mode tells you nothing about dark mode. You are not adjusting one theme, you are maintaining two, and they fail independently.
The trap is indirection. Design tokens are good practice right up until a variable points at something undefined, and the browser quietly falls back to an inherited color that happens to look fine on one background.
This site shipped that too: prose body and heading variables pointed at a token that did not exist in the themed set. In light mode the fallback was legible enough that nobody noticed. In dark mode, bold text inside prose sat well under the contrast floor. Same CSS, same markup, one theme broken.
The lesson is procedural, not technical: every contrast check must be run twice. Once per theme. A single-theme audit is half an audit, and it is the half that passes.
Why your accessibility score is not evidence
Automated accessibility testing is worth running. It is not worth believing.
Automated tools are good at machine-decidable facts: this input has no label, this image has no alt, these two colors have a computed ratio below threshold. That is real value and you should absolutely gate CI on it.
But an enormous share of actual barriers are semantic, and semantics are not machine-decidable:
- Is the alt text accurate, or just present?
- Does the focus order match the reading order, or merely exist?
- Does the error message explain how to fix the problem, or just announce failure?
- Is that
<div role="button">operable with a keyboard, or only clickable? - Does the skip link actually skip to the main content, or to an empty wrapper?
A tool can confirm a skip link exists. Only a person pressing Tab can confirm it goes somewhere useful.
So treat the score the way you treat test coverage: a number that goes down when you break something obvious, and that proves nothing when it is high.
The audit that actually finds things
Fifteen minutes, no tooling budget, no consultant.
1. Unplug the mouse. Tab through the entire page. You are looking for three things: can you reach every interactive element, can you always see where you are, and can you get back out of anything you got into. Dialogs and search overlays are where this breaks.
2. Tab first, before anything else. The very first Tab press should reveal a skip link. Press Enter. Confirm your next Tab lands inside the main content and not back at the top of the navigation.
3. Zoom to 200%. Not browser-width resize — actual zoom. Text should reflow, not clip, and nothing should overlap. This catches fixed heights and absolute positioning.
4. Read the headings alone. Extract every heading in order and read just that list. If it does not work as an outline of the page, it does not work as navigation either.
# every heading on a built page, in document order
grep -oE '<h[1-6][^>]*>[^<]*' dist/posts/some-post/index.html
5. Run both themes through a contrast checker. Body text, muted text, link text, code comments, and any text on a colored background. Both themes. Every time.
6. Turn on reduced motion at the OS level. Your animations should become instant, not merely faster.
None of this needs a budget. All of it finds things a scanner cannot.
Where this leaves static sites
Static architecture is a genuinely good starting position for accessibility. Content-first HTML, real links, no hydration gap, no framework fighting the platform. You begin ahead.
But “ahead” is a starting position, not a result. Contrast, alt text, heading structure, focus visibility, and motion preferences are authored, and no renderer authors them for you. The wins static gives you are the ones you got by not writing bad JavaScript. The remaining failures are the ones you write yourself, in CSS and in prose.
The site you are reading had two real contrast bugs shipped to production while scoring 100 on automated accessibility. Both were found by a person looking, not by a tool running.
Go look at your own.
