AI gives you a dark mode that flashes white, forgets the toggle, and hardcodes one shade of gray everywhere. The token-first prompt that makes theming survive in a single self-contained HTML file.
You ask the AI for a dark mode toggle. It adds one. You click it — the page goes dark, looks great. Then you refresh, and it's light again. You open the file on a phone that's already in system dark mode, and it loads light, flashes, then snaps to dark a beat later. And when you decide the dark background is a touch too black, you go to change it and find #0a0a0a typed out in eleven different places.
None of this is the tool being bad at CSS. It's the prompt being silent on the four things that make a theme actually hold together. Dark mode is deceptively shallow as a request and surprisingly layered as an implementation — and in a single self-contained HTML file, with no framework and no build step to lean on, every layer has to be asked for out loud.
Pull a default AI dark-mode implementation apart and the same four gaps show up:
dark class that re-declares every color again. Editing the palette means hunting the same value through hundreds of lines.prefers-color-scheme.Each maps to one clause you can add to the prompt. Here's the layer that fixes the first one, because everything else sits on top of it.
The single instruction that changes the most: ask for CSS custom properties — design tokens — instead of literal colors.
Don't write color values inline. Define a token layer: every color in the file references a CSS variable like
var(--bg),var(--surface),var(--text),var(--text-muted),var(--border),var(--accent). Put the actual hex values in two blocks only —:rootfor the light palette and[data-theme="dark"]for the dark palette. Switching themes should mean nothing more than flipping thedata-themeattribute on the<html>element.
Now the whole palette lives in roughly a dozen lines, twice. Want a warmer dark? Edit --bg in one place. Want to hand the file to someone to rebrand? They touch two small blocks, not the markup. The toggle stops being a class that re-declares everything and becomes a single attribute flip the variables respond to automatically.
This is also the layer that makes the next three fixes small, because once theme is "the value of one attribute on <html>," resolving it early and saving it are each just a couple of lines.
The white blink is a timing problem. The browser paints with whatever's in the document before your script runs; if the theme-setting script is at the bottom of the body, the paint already happened in light.
Before the page renders, resolve the theme. Put a small inline
<script>as the first thing inside<head>, before any styles or content: read the saved choice fromlocalStorage, fall back toprefers-color-scheme, and set thedata-themeattribute ondocument.documentElementsynchronously. This runs before first paint so there is no flash.
That ordering is the whole trick — the script is tiny, but it has to be first, above the CSS, above the body. Most AI tools put all the JavaScript together at the end by default, which is exactly where it can't help. Naming "first thing in the head, before first paint" moves it.
Theming bugs hide on your own machine — your system's already in the right mode, the cache is warm. Drop the file at a private link and open it cold on your phone to see whether the flash and the system-default actually behave. No card, no account, 7-day self-destruct.
The dropped-preference and ignored-system bugs are the same unanswered question: when the visitor's saved choice and their system setting disagree, which one wins? The model can't infer that from "add a dark mode toggle," so it picks one path and quietly drops the other — usually it either always follows the system (your toggle does nothing on reload) or always follows the toggle (a dark-mode visitor still gets a light first load). Spell out the order and both bugs close at once.
Theme precedence: on first visit, default to the visitor's system setting via
prefers-color-scheme. When the visitor clicks the toggle, that explicit choice overrides the system setting and is written tolocalStorage. On every later visit, the saved choice wins over the system setting. So: saved choice beats system default; the toggle writes the saved choice.
Now the toggle does two jobs — flip the attribute and record the decision — and the head script reads that record before paint. System preference for the newcomer, remembered choice for the returner. That's the behavior people expect without being able to name it, and it falls out of one ordered sentence.
Stacked together, for a one-shot generation:
Build [the thing] as a single self-contained HTML file with a light/dark theme:
- Tokens: all colors reference CSS variables (
--bg,--surface,--text,--text-muted,--border,--accent). Hex values live in two blocks only::root(light) and[data-theme="dark"](dark). No inline hex anywhere else.- No flash: an inline
<script>as the first child of<head>readslocalStoragethenprefers-color-schemeand setsdata-themeon<html>synchronously, before any paint.- Precedence: system setting is the first-visit default; a toggle click overrides it and saves to
localStorage; the saved choice wins on return visits.- Toggle: a visible control that flips
data-themeand persists the choice.Before finishing, verify: no raw hex outside the two palette blocks, the theme script is the first element in
<head>, and the toggle writes tolocalStorage.
The closing verification clause earns its place — it's a second pass where the model catches the inline hex it scattered out of habit. If you're also fighting CDN and font externals, fold this into the broader portable self-contained HTML prompt patterns so theming and portability come out clean in the same generation.
This holds across Claude, ChatGPT canvas, v0, Lovable, Cursor, and Bolt. Each renders a slightly different toggle and palette, but the four-layer structure survives — because it describes CSS and DOM behavior, not anything tool-specific.
Everything above is client-side: CSS variables, one localStorage key, a few lines of JavaScript. No server resolves themes; no database stores preferences. That's precisely why it works on a static host — the toggle is pure browser state, and localStorage is keyed to whatever domain serves the file, so each visitor's choice persists per-origin in their own browser with nothing running behind it. Whether you host the AI artifact at an unguessable link or put it behind a password, the theming behaves exactly as it did locally.
The one honest boundary: if you want a logged-in visitor's theme to follow them across different devices and browsers, that's no longer a single-file job. Syncing a preference to an account needs a server and a database — a real app platform like Vercel or Netlify, with auth and storage. For one shared HTML file, per-device client-side persistence is the right answer and the complete one. The moment you need cross-device sync is the moment you've stopped shipping a file and started running an app.
For everything short of that — a report, a deck, a one-pager, a vibe-coded prototype you want to look right in either mode the second it lands — the four-layer prompt gets you a dark mode that doesn't flash, doesn't forget, and stays one edit away from any palette you want next.
A self-contained HTML file means the images travel inside it, not as links. The prompt wording that makes an AI base64-encode assets on the first pass — plus the size budget, what to inline, and what to leave alone.
A multi-page site usually means a router and a server. Prompt patterns that give you real pages — Home, Pricing, About — inside one self-contained file or one ZIP you can drop at a private link.
Most AI-built HTML looks right on screen and breaks the moment someone hits Ctrl+P. The exact prompt clauses, CSS hooks, and a ninety-second verification pass that make a page export to PDF cleanly the first time.
miinideck turns a single HTML file into an unguessable link with optional password and expiry. Default-private, never indexed.