
How to add syntax highlighting to code blocks in Astro?
Syntax highlighting works in Astro out of the box. Every fenced code block in your Markdown and MDX files, such as one starting with three backticks and ts, is highlighted at build time by Shiki with the github-dark theme, producing HTML with inline styles and no client-side JavaScript. To customise it, set options under markdown.shikiConfig in astro.config.mjs, such as theme, dual themes for light and dark mode, wrap and Shiki transformers for line highlighting and diffs. In .astro files, use the built-in <Code /> component. In Astro 7, the default Markdown processor is Sätteri, and shikiConfig still applies to it, but rehype-based highlighters such as rehype-pretty-code now need the unified() processor from @astrojs/markdown-remark.
Code blocks are a big part of most technical blogs and documentation sites, and good highlighting makes them much easier to read. In this article you'll learn how Astro's default highlighting works, how to choose themes and support dark mode, how to highlight lines and show diffs with transformers, how to highlight code in components, when to use Prism or Expressive Code, and what the move to Sätteri means for highlighting plugins.
How the Default Highlighting Works
Write a fenced code block with a language identifier in any Markdown or MDX file:
```bash
npm create astro@latest
```
Astro highlights it during the build. In an Astro 7.4.2 test project with no configuration, the output was:
<pre class="astro-code github-dark" style="background-color:#24292e;color:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash"><code><span class="line"><span style="color:#B392F0">npm</span><span style="color:#9ECBFF"> create</span><span style="color:#9ECBFF"> astro@latest</span></span></code></pre>
A few things to notice:
- Inline styles: colours are written directly on each token, so no stylesheet or JavaScript is needed.
- The astro-code class: Astro uses
astro-codeinstead of Shiki's usualshikiclass. Keep that in mind when you copy CSS from Shiki's documentation. - tabindex="0": makes the block focusable, so keyboard users can scroll wide code horizontally.
- data-language: records the language, which is handy for adding a language label with CSS.
Shiki supports hundreds of languages, including astro, mdx, diff and svelte. If you leave out the language, the block is rendered as plain text.
Choosing a Theme
Set any of Shiki's built-in themes with markdown.shikiConfig.theme:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
theme: "dracula",
},
},
});
Popular choices include github-light, github-dark, one-dark-pro, nord, vitesse-dark, min-light and catppuccin-mocha. The Shiki website has a gallery of all of them. You can also import a custom theme from a local JSON file and pass the object instead of a name.
Other useful shikiConfig options:
| Option | Purpose |
|---|---|
theme | A single theme for all code blocks |
themes | Several themes, usually light and dark |
defaultColor | Which theme's colours are applied inline; false to use only CSS variables |
wrap | true to wrap long lines instead of scrolling horizontally |
langs | Extra custom language grammars |
langAlias | Map your own aliases to Shiki languages, such as cjs to javascript |
transformers | Shiki transformers that modify the output |
Light and Dark Mode
To support both colour schemes, give Shiki two themes:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
},
},
});
The output then includes both sets of colours. The light theme's colours are applied directly, and the dark theme's are stored in CSS variables:
<pre class="astro-code astro-code-themes github-light github-dark" style="background-color:#fff;--shiki-dark-bg:#24292e;color:#24292e;--shiki-dark:#e1e4e8; overflow-x: auto;" tabindex="0" data-language="bash">
Switch to the dark colours with a small piece of global CSS. For a site that follows the operating system setting:
/* src/styles/global.css */
@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;
}
}
If your site has a theme toggle that adds a class or data attribute to <html>, use that selector instead of the media query, for example html[data-theme="dark"] .astro-code. The dark mode in Astro guide shows how to build the toggle itself.
Highlighting Lines, Diffs and Focus with Transformers
Shiki transformers modify the generated HTML. The official @shikijs/transformers package includes the most commonly needed ones:
npm install -D @shikijs/transformers
// astro.config.mjs
import { defineConfig } from "astro/config";
import {
transformerMetaHighlight,
transformerNotationDiff,
transformerNotationHighlight,
} from "@shikijs/transformers";
export default defineConfig({
markdown: {
shikiConfig: {
themes: {
light: "github-light",
dark: "github-dark",
},
transformers: [
transformerMetaHighlight(),
transformerNotationHighlight(),
transformerNotationDiff(),
],
},
},
});
I tested this with Astro 7's default Sätteri processor and @shikijs/transformers 4.5.0, which matches the Shiki version Astro 7.4.2 uses, and all three worked.
Highlighting Lines from the Code Fence
transformerMetaHighlight() reads line numbers from the code fence's meta string:
```ts {1,3}
const a = 1;
const b = 2;
const c = 3;
```
Lines 1 and 3 get a highlighted class:
<span class="line highlighted">...const a = 1;</span>
<span class="line">...const b = 2;</span>
<span class="line highlighted">...const c = 3;</span>
Ranges such as {1-3,5} work too.
Highlighting with Comments
The notation transformers use special comments in the code itself, which are removed from the output:
```ts
const greeting = "Hello"; // [!code highlight]
const oldName = "Ada"; // [!code --]
const newName = "Grace"; // [!code ++]
```
The result:
<pre class="astro-code ... has-highlighted has-diff" ...>
<span class="line highlighted">...const greeting = "Hello";</span>
<span class="line diff remove">...const oldName = "Ada";</span>
<span class="line diff add">...const newName = "Grace";</span>
The [!code ...] comments disappeared, and the <pre> gained has-highlighted and has-diff classes. Other transformers in the package handle focus ([!code focus]), errors and warnings ([!code error], [!code warning]) and word highlighting.
Styling the Classes
Transformers only add classes. You provide the styles:
/* src/styles/global.css */
.astro-code .line {
display: inline-block;
width: 100%;
}
.astro-code .line.highlighted {
background-color: rgb(255 255 255 / 0.08);
}
.astro-code .line.diff.add {
background-color: rgb(46 160 67 / 0.2);
}
.astro-code .line.diff.remove {
background-color: rgb(248 81 73 / 0.2);
}
.astro-code .line.diff.add::before {
content: "+ ";
}
.astro-code .line.diff.remove::before {
content: "- ";
}
Making each .line an inline block at full width lets the background colour span the whole row. Choose colours that work in both of your themes.
Excluding Languages
Some code fences aren't code to highlight. Diagram tools such as Mermaid read the raw text, so exclude them from highlighting:
// astro.config.mjs (excerpt)
markdown: {
syntaxHighlight: {
type: "shiki",
excludeLangs: ["mermaid", "math"],
},
},
With that setting, a mermaid fence was rendered as a plain <pre><code class="language-mermaid">, ready for a diagram library to process.
Highlighting Code in Astro Components
Markdown settings don't apply to code you render in .astro files. For that, use the <Code /> component from astro:components, which is also powered by Shiki:
---
// src/pages/code.astro
import { Code } from "astro:components";
import { transformerNotationFocus } from "@shikijs/transformers";
const snippet = `const total = items.reduce((sum, item) => sum + item.price, 0);
console.log(total); // [!code focus]`;
---
<Code code={snippet} lang="js" theme="github-dark" />
<Code code={snippet} lang="js" transformers={[transformerNotationFocus()]} />
<p>Run <Code code="npm run build" lang="bash" inline /> to build.</p>
The component takes code, lang and optional props including theme, wrap, inline, defaultColor, transformers, meta and, since Astro 6, embeddedLangs for unusual language combinations. It doesn't inherit your shikiConfig, so pass any theme and transformers explicitly. The inline prop renders a <code> element instead of a <pre>, for highlighted code inside a sentence.
This is useful when code comes from data, such as snippets stored in a content collection, fetched from an API, or read from files at build time.
Using Prism Instead
If you prefer Prism's class-based approach, which lets one stylesheet control colours across the site, switch the highlighter:
// astro.config.mjs
import { defineConfig } from "astro/config";
export default defineConfig({
markdown: {
syntaxHighlight: "prism",
},
});
Code blocks then get Prism's token classes rather than inline styles:
<pre class="language-ts" data-language="ts"><code class="language-ts"><span class="token keyword">const</span> a <span class="token operator">=</span> <span class="token number">1</span><span class="token punctuation">;</span>
You must add a Prism theme stylesheet yourself, for example one from the prism-themes project placed in public/ and linked from your layout. For components, install @astrojs/prism and use its <Prism /> component.
To turn off highlighting entirely, set syntaxHighlight: false. Code blocks are then output as plain <pre><code class="language-...">, which is what you need when a different tool does the highlighting.
Expressive Code for Richer Code Blocks
If you want features like editor and terminal frames, file name titles, copy buttons and line markers without writing your own CSS, Expressive Code is a popular community integration. It's what Astro's own Starlight documentation theme uses.
npx astro add astro-expressive-code
It adds itself to integrations and takes over Markdown code blocks. It reads options from the code fence's meta string:
```ts title="src/utils/greet.ts" {2}
export function greet(name: string) {
return `Hello, ${name}!`;
}
```
In my test, version 0.44.3 worked with Astro 7.4.2 and the default Sätteri processor, rendering a framed block with the file name as its title and line 2 marked. Its package includes a Sätteri plugin alongside the unified one, so no extra Markdown configuration was needed. It ships a small CSS file and a little JavaScript for the copy button.
Because Expressive Code replaces Astro's Shiki handling of Markdown code blocks, configure themes in its own options rather than in shikiConfig.
The Sätteri Change in Astro 7
Before Astro 7, Markdown was processed by the unified ecosystem: remark for Markdown and rehype for HTML. Astro 7 made Sätteri, a Rust-based Markdown and MDX processor, the default. For syntax highlighting, this means:
- Built-in Shiki and Prism still work:
markdown.shikiConfigandmarkdown.syntaxHighlightapply under Sätteri, including Shiki transformers, as the tests above show. - Rehype highlighters need unified: plugins such as
rehype-pretty-codeor@shikijs/rehypeare rehype plugins, so they need theunified()processor. - The old plugin options are deprecated:
markdown.rehypePluginsandmarkdown.remarkPluginsstill work, but only with@astrojs/markdown-remarkinstalled.
When I added rehype-pretty-code through the old markdown.rehypePlugins option without that package, the build stopped with:
`markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` run on the `unified` processor from `@astrojs/markdown-remark`, which is no longer installed by default now that Sätteri is the default Markdown processor. Install it with:
npm install @astrojs/markdown-remark
With the package installed, the same config built but printed a deprecation warning:
[astro] `markdown.remarkPlugins`, `markdown.rehypePlugins`, and `markdown.remarkRehype` are deprecated. Pass them to `unified({...})` from `@astrojs/markdown-remark` directly instead.
The recommended setup passes the plugin to unified():
npm install @astrojs/markdown-remark rehype-pretty-code
// astro.config.mjs
import { defineConfig } from "astro/config";
import { unified } from "@astrojs/markdown-remark";
import rehypePrettyCode from "rehype-pretty-code";
export default defineConfig({
markdown: {
syntaxHighlight: false,
processor: unified({
rehypePlugins: [[rehypePrettyCode, { theme: "github-dark" }]],
}),
},
});
Setting syntaxHighlight: false stops Astro's own Shiki pass from running before the plugin. With this config, rehype-pretty-code 0.14.5 rendered its <figure data-rehype-pretty-code-figure> wrapper, a <figcaption> for the title and data-highlighted-line attributes.
Switching the whole project to unified gives up Sätteri's speed, so only do it if you need a rehype or remark plugin that has no Sätteri equivalent. The same trade-off comes up when building a table of contents with heading plugins.
Choosing an Approach
| Need | Best option |
|---|---|
| Good default highlighting with no setup | Built-in Shiki |
| Light and dark themes | shikiConfig.themes plus a few lines of CSS |
| Line highlights, diffs, focus | @shikijs/transformers in shikiConfig |
| Highlighting code from data in components | <Code /> from astro:components |
| One stylesheet controlling all colours | Prism |
| Frames, titles, copy buttons with no custom CSS | Expressive Code |
| An existing rehype highlighting setup | unified() from @astrojs/markdown-remark |
For MDX content, everything here applies in the same way, as MDX files go through the same processor. See writing blog posts in MDX for MDX setup.
FAQ: Syntax Highlighting in Astro
Yes. Fenced code blocks in Markdown and MDX are highlighted at build time with Shiki using the github-dark theme. The output uses inline styles and adds no client-side JavaScript.
Set markdown.shikiConfig.theme in astro.config.mjs to any built-in Shiki theme name, such as dracula or nord, or use markdown.shikiConfig.themes with light and dark entries for dual themes.
Yes. Sätteri, the default processor in Astro 7, applies markdown.shikiConfig and markdown.syntaxHighlight, including Shiki transformers. Only rehype or remark based highlighting plugins require switching to the unified processor.
Add transformerMetaHighlight from the @shikijs/transformers package to shikiConfig.transformers, then put line numbers in curly braces after the language on the code fence. Style the resulting highlighted class with CSS.
Astro uses the astro-code class instead of shiki. When copying CSS from Shiki's documentation, replace the shiki selector with astro-code.
Astro's built-in highlighting doesn't include one. Either add a small script that inserts a button into each pre element, or install the Expressive Code integration, which includes copy buttons along with frames and titles.
Conclusion
Astro gives you good syntax highlighting with no setup at all: Shiki runs at build time, every code fence in Markdown and MDX gets colours as inline styles, and no JavaScript reaches the browser. From there, markdown.shikiConfig lets you pick themes, support light and dark mode with CSS variables, wrap long lines and add transformers for line highlights, diffs and focus. The <Code /> component covers code rendered from data in .astro files.
Astro 7's move to Sätteri doesn't change any of that, but it does change how plugins fit in. Built-in Shiki, Prism and integrations like Expressive Code work with the default processor, while rehype-based highlighters need @astrojs/markdown-remark and unified(). Choose the simplest option that gives you the code blocks you want, and keep the extra processing for when you really need it.


