---
title: "Test Your Web App in Dark Mode"
url: "https://www.cypress.io/blog/test-your-web-app-in-dark-mode"
description: "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…"
---

# Test Your Web App in Dark Mode

December 13, 2019

By Gleb Bahmutov

Recently, operating systems iOS13, Android 10, MacOS Catalina and Windows 10 have introduced Dark Mode support with [most browsers supporting](https://www.howtogeek.com/440920/browsers-are-bringing-automatic-dark-mode-to-websites/?ref=cypress-io.ghost.io) CSS [prefers-color-scheme](https://developer.mozilla.org/en-US/docs/Web/CSS/@media/prefers-color-scheme?ref=cypress-io.ghost.io). 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!

MacOS Catalina 10.15 System Preferences / General

## 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.

```js
/* 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.

```html
<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 [](https://github.com/bahmutov/todomvc-light-and-dark?ref=cypress-io.ghost.io)[bahmutov/todomvc-light-and-dark](https://github.com/bahmutov/todomvc-light-and-dark?ref=cypress-io.ghost.io) 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.

React TodoMVC example with Light appearance

If you decide to support a Dark appearance, and just flip the background and foreground colors like this

```css
/* dark.css */
body,
.todoapp {
  color: #ddd;
  background-color: #222;
}
```

The result does not look good

Supporting a Dark appearance goes beyond inverting background and font colors

## 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](https://on.cypress.io/browser-launch-api?ref=cypress-io.ghost.io). We can modify the `cypress/plugins/index.js` file like this:

```js
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.

```js
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.

Test running against a website with forced dark media preference

**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](https://peter.sh/experiments/chromium-command-line-switches/?ref=cypress-io.ghost.io) 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.

```html
<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.

```js
/// <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.

```js
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.

The same test running with a different media preference

## Related

Once you are loading the default, dark and light styles in your tests, you can run [accessibility color tests](https://glebbahmutov.com/blog/fix-low-contrast-with-cypress/?ref=cypress-io.ghost.io) using the Cypress plugin [cypress-axe](https://github.com/avanslaars/cypress-axe?ref=cypress-io.ghost.io). You can also make the appearance tests part of your [smart smoke tests](https://www.cypress.io/blog/2019/12/06/use-meaningful-smoke-tests/?ref=cypress-io.ghost.io).