Dark mode shouldn't be hard. Swap some colors, toggle a class, move on. Unless your entire platform is built on the premise that client-side JavaScript doesn't exist — then it gets interesting.
The CSS Variables Bet
Postlark blogs ship as pure HTML and CSS. No framework running on the reader's device, no hydration step, no bundle to download. This gives us near-perfect Core Web Vitals but constrains every design decision. Anything interactive needs to justify its existence in bytes.
Dark mode justified itself quickly. Readers expect it. Eye strain at 2 AM is real, and anyone browsing a developer blog at midnight is probably not reaching for the brightness slider.
The foundation is CSS custom properties — one set of color tokens, remapped through a media query:
:root {
color-scheme: light dark;
--bg: #ffffff;
--text: #111827;
--accent: #2563eb;
--code-bg: #f6f8fa;
}
@media (prefers-color-scheme: dark) {
:root {
--bg: #0f172a;
--text: #f1f5f9;
--accent: #60a5fa;
--code-bg: #1e293b;
}
}
If the reader's OS says "dark," every color on the page flips automatically. No script involved. The color-scheme: light dark declaration even handles native elements like scrollbars and form inputs — a detail most hand-rolled implementations miss entirely.
The 200-Byte Exception
Automatic switching covers most people. But some readers want a manual toggle. Maybe their OS is set to light during work hours, but they prefer dark for reading. Others just like choosing.
A toggle needs state. Remembering a choice across page loads means localStorage. localStorage means JavaScript.
We allowed exactly one exception to the zero-JS rule: an inline script that runs before first paint, reads a theme key from storage, and stamps a data-theme attribute on the document root. That's the entire runtime — around 200 bytes, compressed. The toggle button flips the attribute and writes back to storage. No event listeners polling on scroll, no reactive framework, no animation loop.
Two hundred bytes felt like a reasonable tax for manual control. We drew the line there and haven't needed to revisit it.
Code Blocks
Syntax highlighting in dark mode is a trap. You can't simply invert colors and hope a blue keyword on white doesn't vanish against a dark blue background. Shiki handles this by supporting dual themes — light and dark color sets baked into the HTML as CSS variables during server-side rendering. When the mode switches, tokens flip through the same media query as everything else. No re-render, no flash.
Custom CSS — A Blank Textarea
Dark mode was the default experience. Custom CSS was the escape hatch.
Every Postlark blog on Starter plan or above gets a textarea in the dashboard settings. Write CSS, save, see it live. No build step, no theming engine to learn, no curated gallery of "skins." Raw CSS, applied as a <style> tag on your blog.
To make this useful without forcing people to reverse-engineer the DOM, we expose hooks. The <body> element carries data attributes — blog name, description, domain — so you can target styles conditionally across multiple blogs. A documented set of CSS variables gives access to every color, spacing value, and typographic decision in the default theme. Override what you want, leave the rest alone.
A help modal next to the editor lists the full catalog: variable names, class selectors, data attributes, and a few starter recipes. Someone fluent in CSS can reshape their blog entirely. Someone less confident can paste a recipe and adjust the hex values.
For Creator-tier blogs, header and footer HTML injection extends the surface further. Custom navigation bars, newsletter signup banners, SVG logos replacing the text header — drop in the markup and style it with the same CSS textarea. The platform doesn't care what you put there as long as it's valid HTML.
The Cache Purge Bug
Here's a story from the commit log.
A user changed their custom CSS. The homepage picked up the new styles immediately. Individual post pages didn't — for hours.
The cause was straightforward once we found it. When theme settings change, we purge cached pages so the next visitor gets fresh HTML with the updated stylesheet baked in. But the purge function was only clearing the homepage, the RSS feed, and the sitemap. Post pages, sitting behind a 24-hour CDN cache, kept serving stale markup with the old styles.
The fix was one commit: on theme change, read the post index and purge every post URL alongside the homepage. Thirty-five lines across two files. Not dramatic. But the kind of issue that silently degrades trust — a blogger changes their font color, sees it work on the homepage, and assumes individual posts updated too. They don't check. Their readers see the old design for another twenty-three hours.
Caching is a system-level concern. You can't reason about it page by page.
Getting Images Right
One last thing that's easy to botch: images in dark mode.
The naive approach — filter: invert(1) on everything — destroys photos, mangles diagrams, and turns screenshots into photographic negatives. Some platforms apply a heavy brightness reduction that makes everything look washed out. Neither is acceptable.
We settled on brightness(0.92) contrast(1.1) for article images when dark mode is active. Enough to prevent a bright white screenshot from searing your retinas in a dim room, subtle enough that colors stay true. The first iteration used 0.85, which made photos noticeably muddy. We tested across screenshots, photos, diagrams, and the occasional meme until 0.92 hit the point where you stopped noticing the filter was there.
Images with text overlays can opt out entirely via a data-no-dark attribute. No filter, no surprises.
The Tradeoff
Custom CSS adds zero bytes to the platform's base weight for blogs that don't use it. Dark mode costs 200 bytes of JavaScript and some CSS variables. The whole theming layer is server-rendered, cached at the edge, and degrades gracefully with scripts disabled.
We're not building a theme marketplace or a drag-and-drop page builder. The people publishing on Postlark — developers and technical writers who know what border-radius does and have opinions about it — don't need that. A blank textarea with good documentation beats a hundred toggles in a visual editor. Every time.