420

Windows and macOS now have dark mode.

I know that in CSS I could detect whether the OS is in dark mode like so:

@media (prefers-dark-interface) { 
  color: white; background: black 
}

But I am using the Stripe Elements API, which puts colors in JavaScript, like so:

const stripeElementStyles = {
  base: {
    color: COLORS.darkGrey,
    fontFamily: `-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji"`,
    fontSize: '18px',
    fontSmoothing: 'antialiased',
    '::placeholder': {
      color: COLORS.midgrey
    },
    ':-webkit-autofill': {
      color: COLORS.icyWhite
    }
  }
}

How can I detect the OS's preferred color scheme in JavaScript?

1
  • 2
    I couldn't get the @media (prefers-dark-interface) media query to work in Chrome 80 (like you mentioned), but @media (prefers-color-scheme: dark) did. Commented Mar 31, 2020 at 21:08

11 Answers 11

817
if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
    // dark mode
}

To watch for changes:

window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', event => {
    const newColorScheme = event.matches ? "dark" : "light";
});
Sign up to request clarification or add additional context in comments.

15 Comments

I'll just leaver this here: window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', function(e) { console.log('changed!!');})
Note that prefers-color-scheme: dark does not seem to work in Edge. Not in CSS or Javasript.
@VDWWD old Edge or new Edge (Chromium-based)?
Yes, that's the "old" version. As far as I know, the only way to detect dark mode in the EdgeHTML-based Edge is through the Windows APIs which are only available if the user "installs" the app through the Windows Store. I wouldn't bother with this as the Windows feature update 2004 (rolling out right now) replaces the "old" Egde with the new Chromium-based Edge.
The TypeScript event type for this event handler is event: MediaQueryListEvent.
|
45

You can check the CSS Media-Queries directly with JavaScript

The window.matchMedia() method returns a MediaQueryList object representing the results of the specified CSS media query string. The value of the matchMedia() method can be any of the media features of the CSS @media rule, like min-height, min-width, orientation, etc.

To check if the Media-Query is true, the matches property can be used

// Check to see if Media-Queries are supported
if (window.matchMedia) {
  // Check if the dark-mode Media-Query matches
  if(window.matchMedia('(prefers-color-scheme: dark)').matches){
    // Dark
  } else {
    // Light
  }
} else {
  // Default (when Media-Queries are not supported)
}

Or a simple shorthand for an easy boolean check (with a default at the end):

  const checkIsDarkSchemePreferred = () => window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ?? false;

To update the `color-scheme` dynamically based on the user's preference, the following can be used:
function setColorScheme(scheme) {
  switch(scheme){
    case 'dark':
      console.log('dark');
      
      break;
    case 'light':
      console.log('light');
      // Light
      break;
    default:
      // Default
      console.log('default');
      break;
  }
}

function getPreferredColorScheme() {
  if (window.matchMedia) {
    if(window.matchMedia('(prefers-color-scheme: dark)').matches){
      return 'dark';
    } else {
      return 'light';
    }
  }
  return 'light';
}

function updateColorScheme(){
    setColorScheme(getPreferredColorScheme());
}

if(window.matchMedia){
  var colorSchemeQuery = window.matchMedia('(prefers-color-scheme: dark)');
  colorSchemeQuery.addEventListener('change', updateColorScheme);
}

updateColorScheme();

Comments

18

According to MediaQueryList - Web APIs | MDN, addListener is the correct way to listen to the change. addEventListener is not working for me on iOS 13.4.

window.matchMedia('(prefers-color-scheme: dark)').addListener(function (e) {
  console.log(`changed to ${e.matches ? "dark" : "light"} mode`)
});

3 Comments

Also from MDN - This is basically an alias for EventTarget.addEventListener(), for backwards compatibility purposes.
Just to quote the mentioned MDN, as I was confused and looked it up: " addListener() Adds to the MediaQueryList a callback which is invoked whenever the media query status—whether or not the document matches the media queries in the list—changes. This method exists primarily for backward compatibility; if possible, you should instead use addEventListener() to watch for the change event."
According to MDN it's deprecated
18

Here's a one-liner based on SanBen's answer:

const getPreferredScheme = () => window?.matchMedia?.('(prefers-color-scheme:dark)')?.matches ? 'dark' : 'light';

1 Comment

love the optional chaining. never seen that ?. operator before. very cool.
10

Using optional chaining on matchMedia:

const theme = window.matchMedia?.("(prefers-color-scheme: dark)").matches ? "dark" : "light"

1 Comment

Nice use of optional chaining. Since this only has two results you could make it a boolean called isDark for tighter code.
7

Before typing your JavaScript:

You can use CSS to make a @media query that says to have an ::after pseudo class on the body element that has a different text depending on the color-scheme of the user. In order to make sure the ::after on the body element doesn't confuse users, add a display: none; on the after element.

CSS Code:

@media (prefers-color-scheme:dark){
    body::after{
        content: 'd';
        display: none;
    }
}
@media (prefers-color-scheme:light){
    body::after{
        content: 'l';
        display: none;
    }
}

Now for your JavaScript:

Since we have an object in the document to select, we can get the ::after pseudo class of the body element. We need to get the content of it, just make sure your CSS loads before your JavaScript does! 'd' for dark mode, and 'l' for light mode.

JavaScript Code:

var colorScheme = getComputedStyle(document.body,':after').content;
// d for dark mode, l for light mode.

Why this could be useful

You can do this in CSS and HTML, but why would you do it in JavaScript?

You would use JavaScript because you might have to add an img element, but the image has text, so you got 2 images, one for light mode, the other for dark mode. So, you could use JavaScript to change the src attribute's value of the img element to the correct URL based off of the color-scheme.

There is probably more uses, but that is the use I can think of.

Sources:

I learned getComputedStyle function from this stackoverflow article.

I learned @media (prefers-color-scheme:color scheme to detect) from MDN Web Docs.

I learned how to get .content of computed style from seeing it as a code suggestion on VSCode as I typed getComputedStyle(document.body,':after') and it working as I expected it to. (If I saw it on an article, I can't find which I visited)

1 Comment

Loading different images for dark and light mode, it probably better to use the <picture> tag. One can define fail safe loading, orientation, width, height and color scheme, without using JS at all.
7

Check the matchMedia option:

function getTheme() {
  if(window.matchMedia && window.matchMedia("(prefers-color-scheme:dark)").matches) {
    return "dark";
  } else {
    return "light";
  }
}

2 Comments

Thanks for your contribution - unfortunately this doesn't look like valid JavaScript. If you want to fix it and edit your answer though I'll upvote it!
As it’s currently written, your answer is unclear. Please edit to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers in the help center.
1

If you are using Bootstrap 5 and you want to default to dark if JavaScript is off use this:

// don't hurt peoples eyes /////////////////////////////////////////////////////
const matchPrefersLight = window.matchMedia('(prefers-color-scheme:light)');
if (matchPrefersLight.matches) {
  document.documentElement.setAttribute('data-bs-theme', 'light');
}
matchPrefersLight.addEventListener('change', event => {
  document.documentElement.setAttribute('data-bs-theme', event.matches ? "light" : "dark");
});

And also use your top element:

<html lang="en-US" data-bs-theme="dark">

or whatever other language you are using.

Comments

1

Here's a 1k script that adds appearance-changed event to detect OS theme changes.

// fires every time the OS theme changes
window.addEventListener('appearance-changed', function(e) {
  console.log(e.detail); // `light`, `dark`
});

Also adds window.appearance which you can use to get the current OS theme:

switch (window.appearance) {
    
    case 'light': {
       // do some light theme stuff
    } break;

    case 'dark': {
       // do some dark theme stuff
    } break;
}

It's open source on GitHub.

2 Comments

Probably best not to modify the window object.
I was hoping it might become a standard ;)
0

for people using react, see the useDarkMode hook. Which listens for changes and also allows you to toggle/change the dark mode.

import { useDarkMode } from 'usehooks-ts'

export default function Component() {
  const { isDarkMode, toggle, enable, disable } = useDarkMode()
  return (
    <div>
      <p>Current theme: {isDarkMode ? 'dark' : 'light'}</p>
      <button onClick={toggle}>Toggle</button>
      <button onClick={enable}>Enable</button>
      <button onClick={disable}>Disable</button>
    </div>
  )
}

Comments

-5

You need color-mode If you develop your app using nuxt.

Comments

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.