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.

playbookdark-modeaccessibilityno-code
monito

How to test dark mode: the bugs are in the state, not the colors

playbookdark-modeaccessibilityno-code
August 14, 2026

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.

Go to https://staging.yourapp.com and log in with
test@example.com / Password123!

Switch the app to dark mode using the theme toggle.

Now hunt specifically for elements that are still LIGHT while
everything around them is dark. Look at:
- The page scrollbar, and any scrollbar inside a scrollable panel
- Every dropdown/select — open each one and look at the opened menu
- Any date picker or time picker — open it
- Any file upload button or native input control
- Text inputs after autofill (if the browser offers to autofill a
  saved value, accept it and look at the field)
- The area outside the page content when you scroll past the end

Screenshot every element that is still light-themed or has
unreadable contrast against its surroundings. Be literal — I want
the white rectangles.

"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 localStorage when 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:

Go to https://staging.yourapp.com and log in with
test@example.com / Password123!

Switch the app to dark mode using the theme toggle. Confirm the
UI is dark.

Now check that the preference actually sticks:
1. Reload the page. Is it still dark?
2. Navigate to the settings page, then the billing page, then back
   to the dashboard. Is each one dark on arrival — not light-then-dark?
3. Open a link that does a full page load rather than an in-app
   navigation. Still dark?
4. Log out. Log back in as the same user. Is the app dark, or did it
   reset to light?
5. Go to a page that requires no login (e.g. the marketing home page
   or the login screen itself). Does it respect the dark preference?

Report exactly which of these steps loses the theme, and describe
what you saw at each one — including any moment where the page
appeared light before becoming dark.

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.

Go to https://staging.yourapp.com, log in with
test@example.com / Password123!, and switch to dark mode.

Now deliberately visit the app's less-travelled surfaces and check
each one is properly dark-themed with readable text:
- Submit a form with an empty required field — look at the
  validation error styling
- Trigger an error toast or banner (submit something invalid)
- Open every modal/dialog you can find, including a delete
  confirmation
- Hover something with a tooltip and read it
- Find an empty state — search for a nonsense string that returns
  no results
- Visit https://staging.yourapp.com/this-page-does-not-exist
- Open any dropdown menu, popover, or context menu

For each one report: is it dark-themed, and is the text readable
against its background? Screenshot anything that is light-on-light,
dark-on-dark, or obviously unstyled.

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:

Go to https://staging.yourapp.com, log in with
test@example.com / Password123!, and switch to dark mode.

Walk through the dashboard, settings, and billing pages.

Report every piece of text that is hard to read against its
background. Pay particular attention to:
- Secondary, muted, or "helper" text under form fields and headings
- Placeholder text inside empty inputs
- Disabled buttons and disabled form fields (report these separately —
  they're exempt from WCAG contrast, but I still want to see them)
- Links and any brand-colored text
- Text inside colored badges, chips, or status pills
- Borders or dividers you can barely make out

For each one, tell me the text, where it is, and describe both the
text color and the background color as precisely as you can.
Screenshot each one.

Do not guess at contrast ratios — just report what is hard to read.

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:

Go to https://staging.yourapp.com and log in with
test@example.com / Password123!

Switch the app to dark mode with the theme toggle and confirm the
UI is dark.

Then check the preference survives everything:
- Reload the page
- Navigate to settings, then billing, then back to the dashboard
- Log out and log back in as the same user
- Visit the logged-out home page and the login screen

At each step, tell me whether the app is still dark, and whether you
saw any moment of light before it became dark.

While you're in there: report anything still rendering light in the
dark theme — scrollbars, dropdowns, date pickers, autofilled inputs
— and any text that's hard to read against its background.

Your first run is free — point it at your app in dark mode and find out what your theme forgot.

All Posts