Theme
← Home

Rendering code blocks: options and trade-offs

On this page

Technical posts live or die by their code samples. A good code block is highlighted correctly, readable in light and dark mode, and easy to copy. There are three common ways to get there on a static site.

The three options

Option Highlights JS sent to readers Extras
Plain Shiki (built into Astro) At build time None None out of the box
Expressive Code At build time A tiny copy-button script Titles, terminal frames, marked lines, diffs, copy
Client-side (Prism, highlight.js) In the browser The highlighter + grammars Depends on plugins
  • Plain Shiki uses the same grammars as VS Code, so highlighting is accurate. It adds nothing to the page, but you get bare blocks: no file names, no copy button.
  • Expressive Code wraps Shiki and adds the features deep dives need. It still renders at build time.
  • Client-side highlighters ship JavaScript and grammars to every reader, and code flashes unstyled until they run. They made sense before build-time highlighting was easy; on a static site they rarely do.

Side by side

The same snippet, rendered both ways. First plain Shiki:

export function total(items: { price: number }[]): number {
  return items.reduce((sum, item) => sum + item.price, 0);
}

Then Expressive Code, with a title and a marked line:

src/lib/total.ts
export function total(items: { price: number }[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}

The colours match because both use Shiki underneath. Expressive Code adds the frame, the title, the marked line, and the copy button.

This blog uses Expressive Code. The rest of this post shows what it can do — each feature is a few characters in the Markdown.

A file with a title

Add title="..." after the language:

src/lib/greet.ts
export function greet(name: string): string {
return `Hello, ${name}!`;
}

A terminal

Shell languages get a terminal frame:

Terminal window
pnpm install
pnpm dev

Marked lines

Mark lines with {2-3}, or show insertions and deletions with ins={...} and del={...}:

function total(items) {
// These two lines are highlighted.
return items.reduce((sum, item) => sum + item.price, 0);
}
function total(items) {
return items.map((item) => item.price).reduce((a, b) => a + b);
return items.reduce((sum, item) => sum + item.price, 0);
}

A diff

Use the diff language with a lang hint to keep syntax colours:

const config = {
syntaxHighlight: "shiki",
integrations: [expressiveCode()],
};

Marked text

Mark a word or phrase inside a line with "...":

import expressiveCode from "astro-expressive-code";

Every block also has a copy button in its top-right corner — hover over one to see it. Light and dark themes follow the site’s theme switch.

The trade-off we accepted

Expressive Code must run before MDX and claims every fenced block. That is why Mermaid diagrams need a small plugin to step aside first.