Syntax highlighting that follows the theme
In 2012 I wrote about highlighting code with Google Prettify, and soon after about adding line numbers to it. Download a script, add a class, call prettyPrint() on load. It worked, and Mobifreaks looked a lot less like a terminal.
Fourteen years later, I noticed a post on this very site with no colours in its code at all. Fixing it turned into a nice little lesson, so here it is.
The bug: code that never met the highlighter
My post on hosting a website from a GitHub repo had a YAML workflow, a flow diagram and some JavaScript. All plain white text. Every other post was fine.
The reason: that post was written in HTML, so its code blocks were <pre><code> tags. Astro highlights Markdown code fences while it builds the page. Raw HTML is passed through untouched, so the highlighter never saw those blocks. Swapping them for fences with a language name fixed it:
```yaml
jobs:
deploy:
runs-on: ubuntu-latest
```
The language after the backticks matters. Without it, the highlighter doesn't know which grammar to use, and you're back to plain text.
Prettify then, Shiki now
Prettify ran in the reader's browser. It downloaded, waited for the page, guessed the language, and wrapped words in coloured spans. On a slow 2012 phone, you could watch the code turn colourful.
This site uses Shiki, which comes built into Astro. It does the same wrapping, but at build time, with the same grammars VS Code uses. The reader gets finished HTML with the colours already in it. No script, no flash, nothing to guess.
Nine themes, one code block
Here is the part I'm happy with. This site has nine themes: Auto, Light, Dark, High contrast, Colour-blind safe, Easy reading, Night, Typewriter and Terminal. Until today, code blocks used one dark theme everywhere. On the cream Light theme, that was a black slab in the middle of the page.
Shiki can output two themes at once. And CSS now has light-dark(), which picks one of two colours based on the element's color-scheme. Put those together:
// astro.config.mjs
markdown: {
shikiConfig: {
themes: {
light: 'github-light-high-contrast',
dark: 'github-dark-high-contrast',
},
defaultColor: 'light-dark()',
},
},
Now every coloured word carries both colours, like color: light-dark(#024c1a, #72f088). Every theme on this site already sets color-scheme to light or dark, so the browser picks the right one by itself. I didn't write a single theme-specific rule for code.
One small catch. Shiki puts its own background on the block as an inline style, so I overrode it with the theme's band colour. It's one of the few places where !important is the honest answer:
.prose pre.astro-code {
background-color: var(--bg-band) !important;
}
Checking the contrast
Back in 2012 I picked a Prettify theme because it looked nice. This time I picked GitHub's high-contrast themes on purpose, and then checked every coloured word against the code background in every theme. Every dark theme stayed above 8:1, which is AAA.
The light side had one problem, and of course it was the comments. GitHub's light comment grey is fine on pure white, but on my cream Typewriter paper it dropped to 3.8:1. Faint grey comments are a classic in code themes. A tiny Shiki transformer swaps that one grey for a darker one at build time, and now the lowest score anywhere is 6:1.
What changed in 14 years
- Where it runs. Then, in the reader's browser. Now, once, when the site builds.
- What it costs. Then, a script and a stylesheet. Now, nothing extra to download.
- How it handles themes. Then, one stylesheet, one look. Now, one CSS function and the theme you already chose.
If your blog runs on Astro, try the two-theme setup above. It's five lines, and your code finally matches your dark mode.