How to Add Dark Mode with Cursor (and to Cursor Itself)
Two different jobs share this search. Switch Cursor's own theme in one shortcut, or have it build dark mode into your app without the flash.
How to Add Dark Mode with Cursor
Two different jobs share this search, so start by deciding which one you have:
- You want Cursor itself to look different — the editor chrome, the code colours. That is a two-shortcut job and the first section covers it.
- You want dark mode in the app you are building — a toggle your users click. That is the rest of the page, and the interesting part is not the CSS. It is the flash.
Using Cursor to build dark mode into your app
The prompt-first approach genuinely works for the first pass. Something like:
Add a dark mode toggle. Use CSS custom properties for colours,
persist the choice, and default to the system preference.
Cursor will scaffold the toggle, the variables and the persistence. What it produces is a starting point that usually needs two corrections, both covered below.
Wrong for: if your app has an established design-token system, prompting is the wrong entry point. The generator will invent plausible colour values rather than reach for your tokens, and you will spend longer reconciling them than you would have spent wiring the tokens yourself. Give it the token names in the prompt, or write that layer by hand.
The Tailwind part, and why the version matters
If you are on Tailwind, the dark: variant does the work — but how you enable a manual toggle changed between major versions, and this is the single most common source of "my dark: classes do nothing".
Tailwind v3 takes a config key. In tailwind.config.js:
module.exports = {
darkMode: 'class', // default is 'media'
}
With the default media strategy, dark: follows the operating system and a toggle is impossible — that is the behaviour, not a bug.
Tailwind v4 removed the darkMode config key entirely. Per Tailwind's dark mode documentation, you declare it in CSS instead, after the import:
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
Use :where() rather than :is() here. :is() takes the specificity of its most specific argument, which raises the specificity of every single dark: utility and produces override bugs that are miserable to trace. :where() contributes zero specificity.
In both versions the default with no configuration is the system preference via prefers-color-scheme, so if all you want is "respect the OS", you already have it and need none of the above.
Wrong for: if your dark palette is not a straightforward per-property swap — if dark mode changes layout, swaps imagery, or drops shadows for borders — utility pairs get unreadable fast. Put the divergence in CSS custom properties and let dark: toggle the variable, not forty individual classes.
The flash, which is the actual problem
Here is what prompting alone will not solve, and what most people searching this eventually hit.
You persist the theme in localStorage. You read it in a useEffect and set a class. It works — but on every page load the browser paints the light theme first, then JavaScript runs and switches to dark. A white flash on a dark-mode site, every single navigation.
The cause is ordering: the HTML is painted before your React code applies the class. No amount of CSS fixes it, because the CSS is correct — it is being applied too late. The fix is a small blocking script in the document head that reads storage and sets the class before first paint.
On Next.js, next-themes exists specifically for this and injects that script for you. Two details that are not obvious:
- You must add
suppressHydrationWarningto your<html>element. The library mutates that element before React hydrates, so React will otherwise warn about a server/client mismatch. It applies one level deep only, so it will not mask genuine hydration bugs elsewhere. - The flash can persist in
next devand disappear in a production build. If you are chasing a flash that only reproduces locally, build before you debug further — you may be fixing nothing.
Wrong for: next-themes is the wrong dependency if you are not on Next.js, and arguably the wrong one if you only ever need "follow the OS" with no toggle. In that case a media query alone is fewer moving parts and no client component boundary in your root layout.
What to check before you call it done
- Load with the OS set to dark and no stored preference. Does it open dark?
- Set the toggle to light, reload with the OS in dark. Does your explicit choice win over the system?
- Hard-reload a few times on a slow connection. Any flash?
- Check a form input, a disabled button and a focus ring. Generated dark palettes routinely miss all three.
- Check your
<meta name="theme-color">if you have one, and any inline SVG with hardcoded fills.
Ask Cursor to fix each failure you find individually, pasting the component. That is where it is genuinely faster than writing the CSS — targeted repair against a specific symptom, not the initial architecture.
More Cursor guides:
- Use Composer for multi-file edits
- Debug with Cursor
- Make your app mobile-responsive
- Cursor vs GitHub Copilot, compared
Cursor is free to try.
Related Articles
Ready to Build Something Amazing?
Discover the best AI coding tools, tutorials, and comparisons. Start building your next project today.
Explore All ToolsCurated by developers • Updated 2026 • No pay-to-rank