Light and dark code highlighting in Astro without JavaScript
Configure Shiki with two themes and no default colour, choose between them in CSS with prefers-color-scheme, and use a Shiki transformer, not a rehype plugin, to add file names and a copy button. Includes the contrast catch with the standard GitHub themes.
This site follows the visitor’s light or dark setting and remembers nothing, which rules out a theme toggle and anything that stores a preference. Code blocks needed to follow the same rule. Shiki, which Astro uses for highlighting, can do this with no script at all, but a few details decide whether it works.
Two themes, no default colour
markdown: {
shikiConfig: {
themes: { light: 'github-light-high-contrast', dark: 'github-dark-high-contrast' },
defaultColor: false,
},
},With defaultColor: false, Shiki no longer puts a fixed colour on each token.
It writes both theme colours as custom properties, --shiki-light and
--shiki-dark, and leaves the choice to you. The CSS is then short:
.astro-code,
.astro-code span { color: var(--shiki-light); }
@media (prefers-color-scheme: dark) {
.astro-code,
.astro-code span { color: var(--shiki-dark); }
}I set the block’s background from my own colour tokens instead of the theme’s, so code follows the page rather than the theme’s idea of a background.
Check the contrast of the theme you pick
The standard GitHub light and dark pair failed a 4.5:1 contrast check on
comments when I measured it. The -high-contrast variants of the same themes
passed. The site has a test holding text to a contrast ratio in both schemes,
which is how I found out; I wouldn’t have spotted grey comments by eye.
File names and a copy button: use a transformer
I wanted a file name above a block, written in the fence
(```yaml title="flux/apps.yaml"), and a Copy button. My first thought was
a rehype plugin, and that goes wrong in two ways. Shiki’s own rehype step runs
at a point where a user plugin can’t reliably follow it, and the fence’s meta
string, where the title lives, never reaches a rehype plugin at all.
A Shiki transformer sees both the rendered block and its meta. Its root hook
can wrap the <pre> in a <figure>, add a <figcaption> when there is a
title, and append the button.
The button ships with the hidden attribute, and a small script reveals it. If
the script doesn’t run there is no button, rather than a control that does
nothing.