programming

Getting Eleventy to Do What You Want

A walkthrough of setting up Eleventy 3.x with Tailwind v4 and Shiki syntax highlighting.

#javascript #eleventy #web

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.