Astro ships with Shiki as its default syntax highlighter for Markdown and MDX code blocks. Shiki produces accurate, token-level highlighting by using the same TextMate grammars that VS Code uses, so the output looks great out of the box. The problem is that its default themes are hardcoded colors — and that makes them awkward to integrate with a site that has its own design tokens or supports both a light and a dark mode.
The fix is CSS custom properties. Shiki has two complementary mechanisms: the built-in css-variables theme, which exposes token colors as --astro-code-* properties you define yourself, and a dual-theme setup that writes --shiki-light / --shiki-dark inline on every span so a single CSS rule can swap them at runtime. This guide walks through both approaches from a clean Astro project, explains when to use each, and shows you exactly which properties to set.
How Shiki and Astro work together
When Astro processes a fenced code block in Markdown or MDX, it passes the raw source to Shiki, which tokenizes it and wraps each token in a <span> with an inline style="color: ..." attribute. Those inline styles are why the output looks right even without a CSS file — but they also make theming harder, because you cannot override an inline style without !important.
The CSS variables approaches sidestep this by making Shiki emit style="color: var(--some-property)" instead of a literal hex code. The actual colors live in your stylesheet, where you own them completely.
Astro wraps each highlighted block in a <pre class="astro-code"> element. That class is the selector you target in your CSS.
Approach 1: the css-variables theme
This is the simplest path. You set Shiki's theme to the special string "css-variables", and Shiki emits var(--astro-code-*) references for every token type. You then define those properties anywhere in your CSS — typically :root in your global stylesheet.
Step 1 — configure Astro
Open astro.config.mjs and update the markdown.shikiConfig block:
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
markdown: {
shikiConfig: {
theme: 'css-variables',
},
},
});
Step 2 — define the token variables
Astro's css-variables theme reads from a fixed set of custom properties. Add these to your global CSS file — src/styles/global.css is typical:
/* src/styles/global.css */
:root {
/* Base text and background */
--astro-code-color-text: #24292e;
--astro-code-color-background: #f6f8fa;
/* Token colors */
--astro-code-token-constant: #005cc5;
--astro-code-token-string: #032f62;
--astro-code-token-comment: #6a737d;
--astro-code-token-keyword: #d73a49;
--astro-code-token-parameter: #e36209;
--astro-code-token-function: #6f42c1;
--astro-code-token-string-expression: #22863a;
--astro-code-token-punctuation: #24292e;
--astro-code-token-link: #032f62;
}
That is the complete set. Every property maps directly to a TextMate scope category: --astro-code-token-keyword controls if, return, const, and similar reserved words; --astro-code-token-function controls function names; and so on. You do not need to set all of them — any you omit will fall back to the browser default (usually the inherited text color).
Dark mode with the css-variables theme
Because the colors live in CSS, adding a dark variant is a straightforward media query or selector override:
/* Dark mode via prefers-color-scheme */
@media (prefers-color-scheme: dark) {
:root {
--astro-code-color-text: #c9d1d9;
--astro-code-color-background: #0d1117;
--astro-code-token-constant: #79c0ff;
--astro-code-token-string: #a5d6ff;
--astro-code-token-comment: #8b949e;
--astro-code-token-keyword: #ff7b72;
--astro-code-token-parameter: #ffa657;
--astro-code-token-function: #d2a8ff;
--astro-code-token-string-expression: #7ee787;
--astro-code-token-punctuation: #c9d1d9;
--astro-code-token-link: #a5d6ff;
}
}
Or, if your site uses a JavaScript-toggled class or data attribute:
html[data-theme="dark"] {
--astro-code-color-text: #c9d1d9;
--astro-code-color-background: #0d1117;
--astro-code-token-keyword: #ff7b72;
/* ... remaining tokens */
}
This approach has a key advantage: your design tokens live in one place. If you already have a color palette — whether plain CSS custom properties or Tailwind's @theme block — you can reference those values directly:
:root {
--color-accent: #d73a49;
--astro-code-token-keyword: var(--color-accent);
}
Approach 2: dual themes with --shiki-light and --shiki-dark
The second approach lets you pick any two of Shiki's built-in themes — one for light, one for dark — and have Shiki write both colors as inline CSS variables on every token span. Your CSS then just decides which variable to read based on the current color scheme.
This is the better choice when you want a professionally curated palette from a well-known theme (Github Light, Catppuccin, Dracula, etc.) rather than hand-picking every token color yourself.
Step 1 — configure dual themes
// astro.config.mjs
import { defineConfig } from 'astro/config';
export default defineConfig({
markdown: {
shikiConfig: {
themes: {
light: 'github-light',
dark: 'github-dark',
},
},
},
});
Note the key change: theme (singular) becomes themes (plural) with a light and dark key. You can use any of Shiki's built-in theme names here — run npx shiki themes to see the full list, or browse the Shiki themes gallery.
With this configuration, Shiki generates markup like this for each token:
<span style="--shiki-light:#d73a49;--shiki-dark:#ff7b72">const</span>
Both colors are present in the HTML from the start. CSS decides which one is visible.
Step 2 — activate light and dark via CSS
By default, neither variable is applied — you get no color at all until you write the CSS that reads them. Add the following to your global stylesheet:
/* Light mode — default */
.astro-code,
.astro-code span {
color: var(--shiki-light) !important;
background-color: var(--shiki-light-bg) !important;
font-style: var(--shiki-light-font-style) !important;
font-weight: var(--shiki-light-font-weight) !important;
text-decoration: var(--shiki-light-text-decoration) !important;
}
/* Dark mode — triggered by OS preference */
@media (prefers-color-scheme: dark) {
.astro-code,
.astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
font-style: var(--shiki-dark-font-style) !important;
font-weight: var(--shiki-dark-font-weight) !important;
text-decoration: var(--shiki-dark-text-decoration) !important;
}
}
The !important declarations are necessary here because the inline styles Shiki emits on each span take precedence over class-based rules in the normal cascade. This is a known pattern with the dual-theme approach and is safe in this context because you are targeting a specific component, not setting blanket overrides.
If you use a class-based dark mode toggle instead of prefers-color-scheme, replace the media query with a selector:
html[data-theme="dark"] .astro-code,
html[data-theme="dark"] .astro-code span {
color: var(--shiki-dark) !important;
background-color: var(--shiki-dark-bg) !important;
}
Approach 1 vs. Approach 2: which to choose
- Use the
css-variablestheme when you want your code blocks to match your site's existing design tokens and you are comfortable picking token colors manually. It is the most flexible option and integrates cleanly with a Tailwind v4@themeblock or a custom property system you already have. - Use dual themes when you want recognizable, professionally tested color palettes and do not want to hand-tune every token. You get the full fidelity of real Shiki themes — including semantic highlights, italic comments, bold keywords — with no color guesswork on your part.
Troubleshooting common issues
Code blocks have no color at all
With dual themes, this usually means you forgot to add the CSS that reads the --shiki-light / --shiki-dark variables. Shiki writes the variables into the HTML but applies no default styling. Add the CSS from Step 2 above.
Dark mode is not switching
Check that your dark mode selector matches what your site actually uses. If you are toggling a class on <html> with JavaScript, your CSS must use that same class or attribute. If the site relies on prefers-color-scheme only, make sure you have not accidentally disabled that with a forced data-theme attribute on the root element during testing.
The css-variables theme renders all tokens the same color
This happens when the --astro-code-* properties are not defined. Confirm that your global CSS file is imported in your layout component and that the :root block actually has values set. Inspecting the element in browser DevTools and looking for the custom properties under the :root computed styles is the fastest way to confirm they are present.
Inline styles override my CSS
If you are trying to style .astro-code span without using the var(--shiki-*) approach, your rules will lose to Shiki's inline style attributes. Either use !important deliberately (as shown above) or switch to the css-variables theme, which emits variable references rather than literal colors into those inline styles.
A note on Tailwind CSS v4
If your project uses Tailwind CSS v4's CSS-first configuration with an @theme block, you can wire your code block colors directly into your theme:
@theme {
--color-syntax-keyword: #d73a49;
--color-syntax-string: #032f62;
--color-syntax-comment: #6a737d;
}
:root {
--astro-code-token-keyword: var(--color-syntax-keyword);
--astro-code-token-string: var(--color-syntax-string);
--astro-code-token-comment: var(--color-syntax-comment);
}
This keeps a single source of truth for every color on your site. When you update a palette token, the change propagates to utility classes and syntax highlighting simultaneously.
Wrapping up
Both approaches solve the same problem — rigid, hardcoded highlight colors — but from different angles. The css-variables theme gives you full control at the cost of some manual work; the dual-theme approach gives you proven palettes with minimal setup. Pick the one that matches how your project manages color. Either way, the underlying mechanism is the same: CSS custom properties allow the color definitions to live in your stylesheet where you can override, animate, or swap them however your design requires.