Getting Eleventy to Do What You Want
A walkthrough of setting up Eleventy 3.x with Tailwind v4 and Shiki syntax highlighting.
After a few false starts, I've found a setup that actually works cleanly. Here's the gist.
The Stack
Eleventy 3.x handles templates, Tailwind v4 handles styles (CSS-first, no config file), and Shiki does syntax highlighting at build time — no client-side JS for code blocks.
The trick is running PostCSS and Eleventy as separate parallel processes, both writing to dist/. Eleventy watches for changes in dist/assets/css/ via addWatchTarget to trigger browser reload when CSS changes.
Async Config
Eleventy 3.x supports async config out of the box:
import { createHighlighter } from "shiki";
export default async function (eleventyConfig) {
const highlighter = await createHighlighter({
themes: ["github-light", "github-dark"],
langs: ["javascript", "typescript", "python"],
});
eleventyConfig.amendLibrary("md", (mdLib) => {
mdLib.set({
highlight: (code, lang) =>
highlighter.codeToHtml(code, {
lang: lang || "text",
themes: { light: "github-light", dark: "github-dark" },
}),
});
});
}
The amendLibrary call is key — it lets you modify the markdown-it instance rather than replacing it wholesale.
Dark Mode Without Flash
The classic problem: the page loads in light mode for a split second before JS kicks in. The fix is a tiny inline script in <head> before the CSS link:
(function () {
var saved = localStorage.getItem("theme");
var prefersDark = window.matchMedia("(prefers-color-scheme: dark)").matches;
if (saved === "dark" || (!saved && prefersDark)) {
document.documentElement.classList.add("dark");
}
})();
This runs synchronously before the browser paints, so there's no flash.