Recently, operating systems iOS13, Android 10, MacOS Catalina and Windows 10 have introduced Dark Mode support with most browsers supporting CSS prefers-color-scheme. On Mac, you can pick Light (default), Dark or Auto mode via System Preferences / General options. I believe the Apple UI designers are really in love with this feature - it is placed at the very first position!

Dark and Light styles
If you are building a website or a web application you can specify styles to apply when the user's OS has Dark or Light scheme appearance.
/* default CSS styles */
@media (prefers-color-scheme: dark) {
/* overwrite any default styles when
the user has set the Dark appearance */
}
@media (prefers-color-scheme: light) {
/* overwrite any default styles when
the user has set the Light appearance */
}
I recommend NOT placing the additional overrides in the same CSS file - just like media: print
they will just increase the download size even if they are NOT applied. Instead I recommend placing the overrides in a separate stylesheet resource with media=(prefers-color-scheme: ...)
attribute.
<head>
<meta charset="utf-8">
<title>React • TodoMVC</title>
<link rel="stylesheet" href="node_modules/todomvc-common/base.css">
<link rel="stylesheet" href="node_modules/todomvc-app-css/index.css">
<link rel="stylesheet" media="(prefers-color-scheme: dark)" href="dark.css">
</head>
The dark.css
will only be downloaded and applied if the user's OS has a Dark appearance and the browser supports it.
Note: you can find the source code for this blog post in bahmutov/todomvc-light-and-dark repository.
The Dark appearance might really play havoc with your web application. A typical TodoMVC app with todomvc-app-css
styles using the "standard" black fonts on a white or gray background might look ok.

If you decide to support a Dark appearance, and just flip the background and foreground colors like this
/* dark.css */
body,
.todoapp {
color: #ddd;
background-color: #222;
}
The result does not look good

Forcing Dark appearance
Designing a good Dark appearance style takes work, which brings me to the main topic of this blog post - how do you test your web application using a Light or Dark appearance?
By passing a special Chrome browser command line switch when running End-to-End tests of course! Cypress has a mechanism to specify extra browser flags when launching. We can modify the cypress/plugins/index.js
file like this:
module.exports = (on, config) => {
// `on` is used to hook into various events Cypress emits
// `config` is the resolved Cypress config
// modify browser launch arguments
// https://on.cypress.io/browser-launch-api
on('before:browser:launch', (browser = {}, args) => {
console.log('browser', browser)
if (browser.family === 'chrome') {
console.log('adding dark mode browser flags')
args.push('--force-dark-mode=true')
return args
}
})
}
The command line flag --force-dark-mode=true
will force the Chrome browser to use the Dark appearance even if the host OS has the Light appearance set. Here is an example test that adds a couple of items and completes the first one.
it('adds 2 todos', function () {
cy.visit('/')
cy.get('.new-todo')
.type('learn testing{enter}')
.type('be cool{enter}')
cy.get('.todo-list li').should('have.length', 2)
.first().find('.toggle').check()
cy.contains('li', 'learn testing')
.should('have.class', 'completed')
})
Here is the test in action - with our plugins file forcing Chrome to use Dark appearance styles.

Pro tip: you can see all Chrome command line flags that Cypress uses to launch Chrome, plus your extra command line switches by opening a new tab during testing and going to chrome://version
url.

Forcing a Dark appearance using JavaScript
Unfortunately, I could not find a command line switch to do the opposite: force the Light mode while the host OS has the Dark appearance set. As a work-around I have changed the dark stylesheet link to be loaded using JavaScript based on the window.matchMedia
method call.
<head>
<!-- other styles -->
<script>
if (window.matchMedia('(prefers-color-scheme: dark)').matches) {
const link = document.createElement("link")
link.type = 'text/css';
link.rel = 'stylesheet';
link.href = 'dark.css';
document.head.appendChild(link)
}
</script>
</head>
We can now remove the Chrome command line argument --force-dark-mode=true
from the plugins file. Instead, during tests, we can stub the window.matchMedia
call as needed. For example: cypress/integration/dark-spec.js
can test the Dark appearance.
/// <reference types="cypress" />
beforeEach(() => {
cy.visit('/', {
onBeforeLoad (win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: true,
})
},
})
})
it('adds 2 todos', function () {
cy.get('.new-todo')
.type('learn testing{enter}')
.type('be cool{enter}')
cy.get('.todo-list li').should('have.length', 2)
.first().find('.toggle').check()
cy.contains('li', 'learn testing').should('have.class', 'completed')
})
The test passes, and we can see in the Command Log that the matchMedia
stub was really called.

We can even refactor the test itself into a reusable function to test both the dark and the light appearances. The following spec file runs the same test with two different media preferences.
const visit = (darkAppearance) =>
cy.visit('/', {
onBeforeLoad (win) {
cy.stub(win, 'matchMedia')
.withArgs('(prefers-color-scheme: dark)')
.returns({
matches: darkAppearance,
})
},
})
const addsTodos = () => {
cy.get('.new-todo')
.type('learn testing{enter}')
.type('be cool{enter}')
cy.get('.todo-list li').should('have.length', 2)
.first().find('.toggle').check()
cy.contains('li', 'learn testing').should('have.class', 'completed')
}
it('adds 2 todos with light appearance', function () {
visit(false)
addsTodos()
})
it('adds 2 todos with dark appearance', function () {
visit(true)
addsTodos()
})
Reviewing the spec video that runs through most or all features of the site is a great way to catch weird or incorrect application styles that users might see when their OS prefers the Dark mode.

Related
Once you are loading the default, dark and light styles in your tests, you can run accessibility color tests using the Cypress plugin cypress-axe. You can also make the appearance tests part of your smart smoke tests.