highlight() computes the requested theme, returns it in its result, and never uses it. Every CodeBlock in every app renders github-light, so the component is unusable on a dark code surface.
The defect
src/utils/highlighter.ts:
export async function getHighlighter(): Promise<TSHighlighter> {
if (!highlighterInstance) {
highlighterInstance = await createHighlighter({
theme: 'github-light', // <- singleton, hardcoded
})
}
return highlighterInstance
}const effectiveTheme = theme === 'auto'
? (globalThis.matchMedia?.('(prefers-color-scheme: dark)').matches ? 'github-dark' : 'github-light')
: theme === 'dark' ? 'github-dark' : 'github-light'
result = await highlighter.highlight(code, language) // <- effectiveTheme not passed
return { html: highlighted.html, css: highlighted.css, language, theme: effectiveTheme }effectiveTheme reaches only the returned metadata. The CSS comes from the singleton, which is always github-light.
Observable
for (const theme of ['auto', 'dark']) {
const r = await highlight('bun add x', { language: 'bash', theme, lineNumbers: false })
// both: syntax bg=#ffffff, no dark variant, css length 2777 — byte identical
}The emitted palette is GitHub Light: #24292f, #1a7f37, #cf222e, #0969da, #9a6700. Four of its seven colours have luminance below 0.4, so on a dark code surface the highlighted code is unreadable. .syntax also hardcodes background-color: #ffffff with no prefers-color-scheme block, so it paints a white panel into a dark page.
The returned theme field actively misleads: it reports github-dark while the CSS is github-light.
A second problem with auto
theme: 'auto' reads globalThis.matchMedia, which does not exist on the server. highlight() is called from <script server>, so auto resolves to github-light during SSR unconditionally. Even with the theme wired through, server rendering could never select dark, and a viewer's preference is not knowable at render time anyway.
For a server-rendered component the honest fix is to emit both palettes and let CSS choose, the way the rest of the library handles dark mode: ship the dark rules under @media (prefers-color-scheme: dark) (and a .dark selector for apps with a manual toggle) rather than branching in JS.
Suggested
- Pass the resolved theme into highlighting instead of discarding it. The singleton has to go, or be keyed by theme.
- For
auto, emit both palettes with the dark set behindprefers-color-schemeand.dark, rather than probingmatchMediaon the server. - Until 1 is fixed, do not return
theme: effectiveTheme, because it reports something untrue.
Context
Two apps tried to adopt <CodeBlock> today and both stopped here. Both have dark code surfaces and light/dark modes, so a hardcoded light palette is not something className can paper over: the token colours themselves are wrong, not just the panel background.
Related: stacksjs/stx#2014 (the code prop reaches a client script even when copyable is false).