How to test dark mode: the bugs are in the state, not the colors
Every guide to testing dark mode tells you to flip a DevTools switch and look. That finds the easy bugs. Here's the playbook for the ones that ship: the white scrollbar, the theme that forgets, and the modal nobody restyled.
How to test dark mode: the bugs are in the state, not the colors
Search "how to test dark mode" and you'll be told to open DevTools, emulate prefers-color-scheme: dark, and look at the page. Every result says a version of this. It's fine advice for the first ten minutes and it finds exactly the class of bug you'd have found anyway — the page you were already looking at, in the theme you just switched to, on the screen in front of you.
The dark mode bugs that actually ship don't work like that. They're not on the page you're looking at. They're in the modal you didn't open, the error state you didn't trigger, the scrollbar you stopped seeing years ago, and — most of all — in what happens on the second page load.
Dark mode isn't a color scheme. It's a piece of persisted state that has to survive navigation, reload, and a system preference you don't control, and has to be honored by every component including the ones the browser draws for you. Test it as state and the interesting failures fall out.
Bug 1: the components the browser draws
Start here, because it's the one nearly everyone ships and almost no guide mentions.
You wrote your dark theme with prefers-color-scheme, and MDN describes that media feature accurately: it "is used to detect if a user has requested light or dark color themes. A user indicates their preference through an operating system setting (e.g., light or dark mode) or a user agent setting." So your CSS restyles your components. Good.
But some things on your page aren't your components. The browser draws them. And MDN is explicit that a different property governs those — color-scheme "allows an element to indicate which color schemes it can comfortably be rendered in," and when you declare it, "user agents change the following aspects of the UI chrome to match the used color scheme": the color of the canvas surface, form controls, scrollbars, and "the default colors of other browser-provided UI, such as 'spellcheck' underlines."
Then the line that explains the whole bug, from the same page: "Component authors must use the prefers-color-scheme media feature to support the color schemes on the rest of the elements."
Two properties. Two jobs. Miss color-scheme and you get a beautifully dark app with a bright white scrollbar down the side, a native <select> dropdown that opens as a white rectangle, a date picker that flashbangs you, and autofilled inputs with white backgrounds and dark text nobody can read. Every one of those is invisible in a screenshot of your own components and glaringly obvious to a user.
"I want the white rectangles" is not a joke instruction. It's the entire test.
Bug 2: the theme that forgets
The second class, and the one users actually complain about.
The theme is state. It lives somewhere — localStorage, a cookie, a user preference on your server, or nowhere at all — and every one of those has a distinct failure:
- Reload reverts to light. The preference was never persisted, or it's persisted but read too late.
- The preference survives reload but not navigation. A route that renders on the server without knowing the preference.
- The preference survives on one device and not another. It's in
localStoragewhen your users assume it's on their account. - Log out, log back in, and it's light again. The preference was tied to the session rather than the user.
And the related one everybody has seen and nobody tests: the flash of the wrong theme. You load a page in dark mode, get a white flash, then it goes dark. That's the theme being decided after first paint — typically by JavaScript that reads the stored preference once the document is already on screen.
Be honest about what an agent can and can't do here. A sub-100ms flash on first paint is a rendering-timeline question, and a browser agent watching a page load is not a reliable instrument for it — you'd want a paint trace. But the flash and the forgetting have the same root cause: the theme is being decided too late, by the wrong layer. And the forgetting is trivially testable. Test the thing you can measure, and it points at the thing you can't:
Step 5 catches the split-brain version: the app is dark and the logged-out marketing pages are blindingly light, because they're a different bundle reading a different source of truth. Very common. Never noticed by the team, because the team is always logged in.
Bug 3: the surfaces nobody restyled
Dark mode gets applied to the app you demo. It doesn't get applied to the parts of the app you only see when something goes wrong.
The pattern is identical to what happens with untranslated strings in an internationalized app: the neglected surfaces are the ones you reach by doing something wrong, and they're neglected because the happy path is the only path anyone walks manually.
Light-on-light and dark-on-dark are the two shapes this takes. An error message styled color: #c00 on a background that used to be white and is now #111 is still red — it's just red on near-black, which is a hard read and an accessibility failure. Which brings us to the one that isn't cosmetic.
Bug 4: contrast that got worse in the dark
Here's the part that makes dark mode an accessibility question rather than a taste question: dark themes routinely fail contrast in places their light counterparts passed.
The bar is WCAG 2.2 Success Criterion 1.4.3, Contrast (Minimum) at Level AA: a contrast ratio of at least 4.5:1 for normal text, and at least 3:1 for large text (18pt, or 14pt bold). The W3C is specific that these "are intended to be treated as threshold values" and that computed values shouldn't be rounded — 4.499:1 does not pass.
Where dark themes lose it, predictably: secondary and muted text, which was a perfectly readable mid-grey on white and is now a mid-grey on near-black at about 3:1. Placeholder text, which was already the lightest thing on the page — and which the W3C is explicit is in scope: the criterion "applies to text in the page, including placeholder text and text that is shown when a pointer is hovering over an object or when an object has keyboard focus." Brand colors — a saturated blue that had beautiful contrast against white has poor contrast against #111, and nobody re-picked it for dark. Borders and dividers that vanish entirely.
One thing to get right before you file tickets: disabled controls are exempt. WCAG's incidental exception covers text "that is part of an inactive user interface component," and the W3C spells it out — "User Interface Components that are not available for user interaction (e.g., a disabled control in HTML) are not required to meet contrast requirements." A washed-out disabled button in your dark theme may still be worth fixing as a UX call, but it is not a conformance failure, and claiming it is will get your whole report ignored.
An agent reading a rendered page is not a contrast-ratio calculator, and claiming otherwise would be exactly the kind of overclaim that makes tooling untrustworthy. Use a contrast checker for the numbers. What the agent gives you is the inventory — which surfaces exist, in which states, under this theme — and a legibility read from something with no memory of what the text is supposed to say:
That output is a work list. Take the elements it flags, run them through a contrast checker, and fix the ones under 4.5:1. The agent found the fifteen places to check; you'd have checked three.
The thing you can't test this way
One honest limit, stated plainly.
These prompts all drive the app's own theme toggle. They do not emulate an operating system that's set to dark. A browser agent navigates and clicks, the way a person does — there's no config knob that makes it a Mac in dark mode. So if your app has no in-app toggle and derives its theme purely from prefers-color-scheme, none of the above reaches your dark theme at all, and you need DevTools emulation or a scripted run with the emulation flag set. That's a real gap and it's the one place the DevTools advice in every other article is the right answer.
In practice most apps ship a three-way toggle — light, dark, system — which means the agent can reach dark directly, and the "system" option is a separate, smaller test that does need emulation. Know which one you have before you wire any of this up.
Wiring it into the release
Save each prompt as a Test Scenario and run the four against staging on every release. Each Test Run is roughly 8–13 credits — about $0.08–$0.13 — so the whole dark-mode set costs well under fifty cents and runs while you're reading the PR.
When one fails you get the Monito Session: the screenshot timeline, the console output, the agent's step-by-step reasoning. For theme bugs the screenshots are the bug report — a picture of the white date picker in your dark app is not a claim anyone argues with. Wire the set into CI and the theme regression fails the PR that caused it, which is meaningfully better than finding it in a screenshot a customer posts.
This pairs naturally with visual regression testing — that catches the pixel that changed between two runs, this catches the surface that was never styled in the first place. Different failures, and you want both. And as always, the reason these survive is that they describe intent rather than selectors: rename every class in your theme system and the prompt "find the white rectangles" still runs. That's the argument in why AI QA agents find bugs your scripts miss, applied to a surface where the assertions would otherwise be unwritable.
The one to run first
If you only run one, run the state check — the theme that forgets is the bug your users report, and it takes one prompt:
Your first run is free — point it at your app in dark mode and find out what your theme forgot.