Syntax Highlighting
Code blocks are the backbone of technical writing, and Astro highlights them out of the box with no plugins to install. Fenced code in your Markdown and MDX is colorized at build time using Shiki, the same engine that powers VS Code. Because the work happens during the build, the resulting HTML carries inline styles and ships zero JavaScript to the browser — your readers get pixel-accurate highlighting without paying a runtime cost. This page covers how Shiki works in Astro, how to pick and switch themes, and how to fall back to Prism if you prefer it.
How highlighting works
When Astro renders Markdown, every fenced code block is passed to Shiki. The language tag after the opening backticks tells Shiki which grammar to apply, and the configured theme determines the colors. The output is a plain <pre><code> element with style attributes baked in — no client-side <script>, no flash of unhighlighted code.
```ts
const greeting: string = "Hello, Astro";
console.log(greeting.toUpperCase());
```
Shiki ships with hundreds of TextMate grammars (ts, astro, bash, json, python, and many more) and dozens of themes, so most projects never need additional configuration. The language identifier is required for accurate tokenization; an unknown or missing tag falls back to plain, uncolored text.
Highlighting runs at build time. If you change the theme, you must rebuild (or restart
astro dev) for existing pages to pick up the new colors — there is no runtime restyling.
Configuring Shiki
All highlighting options live under markdown.shikiConfig in astro.config.mjs. The most common change is selecting a theme. You can pass a single theme by name, or a themes object to enable dual light/dark output.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "dracula",
wrap: true,
},
},
});
| Option | Type | Description |
|---|---|---|
theme | string | A single bundled theme name, e.g. "github-dark" |
themes | object | { light, dark } pair for dual-theme output |
langs | array | Extra languages or custom TextMate grammars to load |
wrap | boolean | null | true wraps long lines, false adds a scrollbar, null overflows |
transformers | array | Shiki transformers to mutate the generated HTML/tokens |
Dual light and dark themes
Pass a themes object to emit CSS variables for both modes in a single build. Astro outputs --shiki-light and --shiki-dark custom properties; you toggle between them with a tiny bit of CSS keyed off your color-scheme switch.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
},
},
});
---
// src/components/ThemeStyles.astro — switch Shiki vars by data attribute
---
<style is:global>
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;
}
</style>
Adding languages and transformers
If you use a niche language not in the default bundle, register it under langs. You can also wire in Shiki transformers to add features like highlighted lines, diff markers, or focus blocks — these run at build time and decorate the generated markup.
// astro.config.mjs
import { defineConfig } from "astro/config";
import { transformerNotationDiff } from "@shikijs/transformers";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "nord",
langs: ["astro", "prisma"],
transformers: [transformerNotationDiff()],
},
},
});
With transformerNotationDiff, a comment like // [!code ++] on a line marks it as an addition and // [!code --] marks a removal, letting you render change diffs inside ordinary code blocks.
Switching to Prism
Astro also bundles support for Prism if you prefer its ecosystem or already maintain a Prism theme. Set markdown.syntaxHighlight to "prism". Unlike Shiki, Prism does not inject inline styles — you must import a Prism stylesheet yourself so the token classes have colors.
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
syntaxHighlight: "prism",
},
});
---
// src/layouts/BaseLayout.astro — load a Prism theme stylesheet
import "prismjs/themes/prism-tomorrow.css";
---
<slot />
You can disable highlighting entirely with syntaxHighlight: false, which is occasionally useful when an external tool or a custom Rehype plugin owns the code formatting.
syntaxHighlight value | Highlighter | Styling source |
|---|---|---|
"shiki" (default) | Shiki | Inline styles, no extra CSS needed |
"prism" | Prism | Requires a Prism theme stylesheet |
false | None | Plain <code>, style it yourself |
Shiki and Prism are mutually exclusive per build. Pick one highlighter for the whole site rather than mixing them, since their class names and styling models differ.
Best practices
- Always tag fenced blocks with the correct language so Shiki tokenizes accurately; an unlabeled block renders as plain text.
- Prefer the default Shiki highlighter — its inline styles avoid a flash of unstyled code and need no extra CSS bundle.
- Use a
themeslight/dark pair instead of two separate builds so a single deployment serves both color schemes. - Reach for transformers (diff, highlighted lines, focus) before writing custom Rehype code that re-parses your blocks.
- If you choose Prism, remember to import a Prism theme stylesheet, or your code will render unstyled.
- Rebuild after changing any
shikiConfigvalue — highlighting is static and will not update existing pages on its own.