docs/solutions/ui-bugs/2026-05-25-docs-code-block-light-theme-must-use-light-shiki-tokens.md
Changing the docs code-block container to a light bg-code surface is not
enough. If rehype-pretty-code still emits only dark-theme tokens, the code
looks washed out or strangely colored on the light background.
The same rule applies to client-rendered react-syntax-highlighter blocks:
the wrapper surface, token palette, and theme switching behavior all need to
agree.
Line numbers have the same coupling problem: showLineNumbers only marks the
code block. The rendered line nodes also need to match the CSS selector that
draws the counters.
pre block and npm-command block do not visually match shadcn's
light code style.bg-code wrapper while the token spans keep light
theme colors.showLineNumbers emit data-line-numbers, but no visible
numbers appear because each line renders as class="line" instead of matching
the docs [data-line] counter selector.github-light-default throws:ShikiError: Theme github-light-default is not included in this bundle
pre/command wrapper classes to bg-code fixed the
container but left token colors wrong.github-light-default theme name matched upstream intent, but
the current Plate Shiki bundle does not ship that theme.pre prop object into the command component created a ref
type mismatch because the command wrapper renders a div, not a pre.react-syntax-highlighter instance with dark:hidden still mounts
and tokenizes both themes.useTheme() on the first
client render can create a server/client inline-style mismatch that React does
not reliably patch.background, backgroundColor, or
border on the rendered surface conflicts with the docs bg-code wrapper and
can trigger React style shorthand warnings during theme updates.showLineNumbers metadata alone is incomplete if onVisitLine does
not add the attribute expected by the docs CSS.showLineNumbers before rehype-pretty-code is still not enough if
later post-processing handles the generated fragment but never restores
data-line-numbers onto each emitted code element.Configure rehype-pretty-code with explicit light and dark themes that exist in
the current bundle:
theme: {
dark: 'github-dark',
light: 'github-light',
},
Keep metadata propagation theme-aware. The current rehype-pretty-code output
can contain separate light and dark pre[data-theme] elements, so apply
__rawString__, __src__, __event__, __style__, and __withMeta__ to each
generated pre.
Add CSS display rules for the emitted theme variants:
[data-rehype-pretty-code-fragment] > :not(code)[data-theme="light"] {
display: block;
}
[data-rehype-pretty-code-fragment] > :not(code)[data-theme="dark"] {
display: none;
}
.dark [data-rehype-pretty-code-fragment] > :not(code)[data-theme="light"] {
display: none;
}
.dark [data-rehype-pretty-code-fragment] > :not(code)[data-theme="dark"] {
display: block;
}
For command blocks, preserve only the relevant generated attributes such as
data-language and data-theme when routing the pre through the custom
command renderer.
For line numbers, keep the metadata and line-node selector in sync. Add
showLineNumbers to multi-line source snippets before rehype-pretty-code
runs, and make every visited line expose the attribute used by the existing CSS:
onVisitLine(node: any) {
node.properties['data-line'] = '';
if (node.children.length === 0) {
node.children = [{ type: 'text', value: ' ' }];
}
}
When post-processing rehype-pretty-code fragments, carry a private boolean
from the original pre node and apply data-line-numbers to every generated
pre > code child. This is more robust than depending only on synthetic meta,
especially when dual light/dark pre[data-theme] nodes are emitted.
Client-rendered highlighters should make the same source-vs-command distinction: show line numbers for multi-line source code and keep single-line install commands clean.
For React SyntaxHighlighter-backed surfaces, render exactly one highlighter and select its theme in a shared client component:
const theme = mounted && resolvedTheme === 'dark' ? darkTheme : lightTheme;
return (
<SyntaxHighlighter {...props} style={theme as any}>
{children}
</SyntaxHighlighter>
);
Keep the first render deterministic by using the light theme until the component mounts. Normalize imported Prism themes so the wrapper owns background and border styling:
function normalizeTheme(theme: Record<string, React.CSSProperties>) {
const {
background: _preBackground,
backgroundColor: _preBackgroundColor,
border: _preBorder,
...preStyle
} = theme['pre[class*="language-"]'] ?? {};
return {
...theme,
['pre[class*="language-"]']: preStyle,
};
}
The visible code surface and the syntax token palette must come from the same theme mode. Dual-theme Shiki output gives the page both token palettes, and the CSS rules choose the correct one for the active document theme.
Preserving data-theme also keeps command blocks in the same visibility system
as normal code blocks instead of treating them as a special case.
For line numbers, rehype-pretty-code sets data-line-numbers on the code
element, while the docs counter CSS increments on each line. Adding data-line
to onVisitLine connects those two halves.
For client highlighters, the initial server-rendered style and the first client
render must match. After mount, a normal state update can switch to the active
next-themes mode and React will patch the token styles. Rendering a single
highlighter also avoids doubling Prism work on release notes and other
code-heavy pages.
rehype-pretty-code dual-theme output, verify one light block is visible
in light mode and the dark duplicate is hidden.data-line-numbers on code and data-line on each line.data-line exists but numbers are invisible, check whether the generated
code still has data-line-numbers after all post-processing plugins run.pre props into a non-pre wrapper without checking refs and
element-specific attributes.