Skip to main content
Back to Blog frontend

Syntax highlighting with Shiki in Astro

Tom Hermans

Table Of Contents

Configuring Shiki Themes in Astro

Syntax highlighting can make or break the reading experience in technical blogs. Astro uses Shiki for code highlighting, and while the default GitHub Dark theme works fine, you might want something that matches your site’s design system. Getting themes configured properly, especially with MDX and dark mode support, isn’t immediately obvious from the docs.

The Basic Setup

The simplest way to change your Shiki theme is to use one of the built-in themes. You don’t need to import anything or download theme files. Just reference the theme name as a string in your Astro config.

import { defineConfig } from "astro/config";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "dracula",
    },
  },
});

Shiki includes dozens of themes out of the box. Visit the Shiki themes gallery (opens in a new tab) to browse options like Nord, Monokai, Gruvbox, and many others. Pick a theme name from that page and drop it into your config as a string. That’s it.

The MDX Gotcha

Here’s where things get tricky. If you’re using the @astrojs/mdx integration, the markdown.shikiConfig setting only applies to .md files. Your MDX files will still use the default theme, which explains why you might configure a theme and see no changes.

MDX requires its own theme configuration within the integration itself. You need to specify the theme in both places if you’re using both markdown and MDX files.

import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: "gruvbox-dark-medium",
    },
  },
  integrations: [
    mdx({
      shikiConfig: {
        theme: "gruvbox-dark-medium",
      },
    }),
  ],
});

This duplication feels redundant, but it’s necessary because MDX processes code blocks differently than standard markdown. After changing the config, restart your dev server. Shiki theme changes won’t hot-reload.

Supporting Light and Dark Modes

Modern sites need to support user theme preferences. Hardcoding a single dark theme means light mode users get a jarring dark code block on an otherwise light page. Astro and Shiki support dual themes through CSS custom properties.

Instead of specifying a single theme, provide both light and dark options using the themes object syntax.

export default defineConfig({
  markdown: {
    shikiConfig: {
      themes: {
        light: 'github-light',
        dark: 'gruvbox-dark-medium',
      },
    },
  },
  integrations: [
    mdx({
      shikiConfig: {
        themes: {
          light: 'github-light',
          dark: 'gruvbox-dark-medium',
        },
      },
    }),
  ],
});

This configuration generates CSS custom properties that you can hook into with your theme switcher. Shiki creates variables like --shiki-light, --shiki-dark, --shiki-light-bg, and --shiki-dark-bg for each token in your code blocks.

Wiring Up Your Theme Switcher

With dual themes configured, you need CSS to apply the correct theme based on your site’s current mode. Astro generates code blocks with the .astro-code class, so that’s what you’ll target.

The exact CSS depends on how your site tracks theme state. Most implementations use a data attribute or class on the HTML element. Here’s an example using a data-theme attribute:

.astro-code,
.astro-code span {
  color: var(--shiki-light);
  background-color: var(--shiki-light-bg);
}

html[data-theme="dark"] .astro-code,
html[data-theme="dark"] .astro-code span {
  color: var(--shiki-dark);
  background-color: var(--shiki-dark-bg);
}

If your theme switcher uses a different pattern like [data-color-mode="dark"] or a .dark class, adjust the selector accordingly. The key is matching however your JavaScript sets the theme state.

Some frameworks use prefers-color-scheme media queries instead of JavaScript theme switchers. That works too:

.astro-code,
.astro-code span {
  color: var(--shiki-light);
  background-color: var(--shiki-light-bg);
}

@media (prefers-color-scheme: dark) {
  .astro-code,
  .astro-code span {
    color: var(--shiki-dark);
    background-color: var(--shiki-dark-bg);
  }
}

This approach respects the user’s system preferences without requiring any JavaScript theme toggle.

Understanding the Generated Markup

When you configure dual themes, Astro generates inline styles on every <span> inside your code blocks. These inline styles define both the light and dark color values using custom properties.

<span style="color: var(--shiki-light); background-color: var(--shiki-light-bg);">

Your CSS then redefines what those custom properties point to based on the current theme. The inline styles don’t change, but their referenced values do. This is why you need to target both .astro-code and .astro-code span in your CSS.

Debugging Common Issues

If your theme isn’t showing up, check a few things. First, verify that you’ve restarted your dev server. Theme changes won’t appear until you restart. Second, inspect the generated HTML. Look for <pre class="astro-code"> elements. If they still say github-dark, your config isn’t being applied.

For MDX files specifically, make sure you’ve added the shikiConfig to the MDX integration, not just the top-level markdown config. This is the most common mistake. The two configs are independent.

If dual themes aren’t working, check your browser’s inspector. The custom properties should be visible on each span element. If they’re missing, your Astro config might have a syntax error. If they’re present but not changing colors, your CSS selector doesn’t match your theme switcher’s implementation.

Custom Theme Files

The built-in themes cover most use cases, but you might want a completely custom color scheme. Shiki accepts VS Code theme JSON files. Export a theme from VS Code or create one following the TextMate grammar format, then import it in your Astro config.

import customTheme from "./my-theme.json";

export default defineConfig({
  markdown: {
    shikiConfig: {
      theme: customTheme,
    },
  },
});

The JSON file needs specific properties like name, type, and colors. Look at existing Shiki theme files for reference structure. This approach gives you complete control but requires more setup than using a built-in theme.

Performance Considerations

Shiki runs at build time, not in the browser. This means syntax highlighting adds zero JavaScript to your page. The tradeoff is larger HTML files since every token gets its own styled span element. For most blogs, this is fine. The increased HTML size is negligible compared to images and other assets.

Dual themes do increase HTML size more than single themes because of the extra custom properties. Each span gets inline styles defining both light and dark colors. On a typical blog post with a few code blocks, this adds maybe 2-3kb. Worth it for seamless theme switching.

Putting It All Together

A complete setup for a modern Astro site with MDX and theme switching looks like this:

import { defineConfig } from "astro/config";
import mdx from "@astrojs/mdx";
import tailwind from "@astrojs/tailwind";

export default defineConfig({
  markdown: {
    shikiConfig: {
      themes: {
        light: 'github-light',
        dark: 'github-dark',
      },
    },
  },
  integrations: [
    mdx({
      shikiConfig: {
        themes: {
          light: 'github-light',
          dark: 'github-dark',
        },
      },
    }),
    tailwind(),
  ],
});

Combined with this CSS in your global styles:

.astro-code,
.astro-code span {
  color: var(--shiki-light);
  background-color: var(--shiki-light-bg);
}

html[data-theme="dark"] .astro-code,
html[data-theme="dark"] .astro-code span {
  color: var(--shiki-dark);
  background-color: var(--shiki-dark-bg);
}

This gives you beautiful syntax highlighting that respects user preferences and works across both markdown and MDX files. No client-side JavaScript required, no flash of unstyled content, just fast, accessible code blocks that look great in any theme.

Links and documentation

Shiki (opens in a new tab)

Shiki in Astro (opens in a new tab)

Back to Blog

Let's Talk

Tom avatar

Get in touch

I work at the intersection of design and code.
Interested? Hit me up.
tomhermans@gmail.com

Copyright © 2026 Tom Hermans. Made by Tom Hermans .

All rights reserved 2026 inc Tom Hermans