How I added syntax highlighting to my site in about 20 lines of code
Tuesday, March 10, 2026.
Code blocks on my writing and notes pages used to render as raw monochrome text on a grey background. Readable, but not particularly inviting for a post that's trying to explain something technical. I wanted proper syntax highlighting — the kind where keywords light up in one colour, strings in another, and comments fade out.
I assumed this would be a weekend rabbit hole. It wasn't. Here's the whole thing.
A quick mental model first
When you write a blog post in Markdown (or MDX, which is Markdown that can include React components), your content isn't displayed as-is. It goes through a compilation pipeline: a series of small programs that each transform the text before it becomes HTML on the page.
Think of it like a design file moving through a handoff workflow. The raw Figma frame passes through auto-layout → component logic → developer inspection → production asset. At each stage, something meaningful happens to the content.
In MDX, that pipeline has two stages:
- remark plugins — work on the raw Markdown (text level)
- rehype plugins — work on the HTML that Markdown becomes (element level)
Syntax highlighting is a rehype concern: by the time we want to colour code, it's already been parsed into <pre> and <code> HTML elements.
The two libraries doing the work
Shiki is a syntax highlighter that uses the same grammar files as VS Code. So if you've ever looked at TypeScript in VS Code and thought "that looks correct and clear" — that's Shiki's logic. It tokenises every keyword, string, comment, and operator, and assigns each one a colour from a chosen theme.
rehype-pretty-code is a plugin that wires Shiki into the rehype pipeline. You configure it once, and it automatically intercepts every fenced code block in your Markdown and runs it through Shiki before the page is rendered.
The key detail: all of this happens at build time, not in the browser. By the time someone loads a page, the colour information is already baked into the HTML as inline styles. No JavaScript runs in the browser to produce the highlighting. For a statically generated site like mine, this is the ideal model.
What the implementation actually looked like
I had two content pages (writing and notes) each passing the same configuration object to the MDX renderer. The first step was pulling that into one shared file:
// lib/mdx-options.ts
import remarkGfm from "remark-gfm";
import rehypeSlug from "rehype-slug";
import rehypePrettyCode from "rehype-pretty-code";
export const mdxOptions = {
remarkPlugins: [remarkGfm],
rehypePlugins: [
rehypeSlug,
[rehypePrettyCode, { theme: "github-light", keepBackground: false }],
],
};Then each page replaced its old inline config:
// Before
options={{ mdxOptions: { remarkPlugins: [remarkGfm], rehypePlugins: [rehypeSlug] } }}
// After
options={{ mdxOptions }}That's it for the logic. The last piece was CSS — styling the wrapper that rehype-pretty-code generates around each code block:
.entry__content [data-rehype-pretty-code-figure] pre {
background: #f6f8fa;
border: 1px solid #e1e4e8;
border-radius: 6px;
padding: 1.6rem 2rem;
overflow-x: auto;
}One thing worth noting: keepBackground: false in the config tells Shiki not to write a background colour as an inline style. That keeps the CSS in control of the background, rather than having to fight Shiki's inline styles with higher-specificity selectors.
The thing I found most interesting
The language label in the top-right corner of each block (you can see "typescript" and "tsx" and "css" above) requires no additional markup. It comes from a CSS trick:
pre[data-language]::before {
content: attr(data-language);
}rehype-pretty-code adds a data-language attribute to the <pre> element. The ::before pseudo-element reads that attribute value and displays it as text. No JSX, no extra components, no JavaScript — just CSS reading a data attribute that the plugin wrote.
This is a pattern worth remembering: HTML attributes are queryable in CSS. When a library annotates elements with data-* attributes, you can use them to drive visual behaviour without touching the component code at all.
If you want to try this yourself
The stack I'm using: Next.js with MDX via next-mdx-remote. The packages are rehype-pretty-code and shiki. There are only a few dozen themes to choose from (github-light, github-dark, nord, dracula, and others) — pick whichever matches your site's aesthetic. The Shiki documentation has a full list.
The whole change — install, config, CSS — took a couple of minutes, most of which was figuring out how implement syntax highlighting in my context and using an coding assistant to one-shot the implementation.
Sometimes the right tool for a job is a very small plugin with a very focused purpose.