DEV Community

Cover image for Auto Dark Mode w/ JSS
Eugene Ghanizadeh
Eugene Ghanizadeh

Posted on

Auto Dark Mode w/ JSS

Recently I've built a little library, called themed-jss for theming my web apps. It enables straightforward and flexible theming, and more importantly it automatically takes care of all additional CSS rules (and JS) needed for proper dark mode support.

In this post I will explain the problem I was trying to solve and then the solution (i.e., the lib, themed-jss). Hope you find it useful in your web apps as well.

 

Theming

A theme is a set of recurring values used in your web apps style. For example, all buttons might be blue, all text might be black and the general background of the app might be white:

const myTheme = {
  buttons: 'blue',
  text: 'black',
  background: 'white'
}
Enter fullscreen mode Exit fullscreen mode

It is useful to keep all of this in one place. Imagine for example that you decide to change the color of all buttons to green instead, if there is no central theme, you need to go through all of your app and update styles.

 

Darkmoding

Dark mode support is (or should be) just specifying an additional theme for your app (how the colors should change in dark mode):

const myTheme = {
  button: 'blue',
  background: 'white',
  color: 'black'
}

const myDarkTheme = {
  ...myTheme,
  background: 'black',
  color: 'white'
}
Enter fullscreen mode Exit fullscreen mode

Unfortunately though, in practice you need more work to add dark mode support to your web app. To get started, you need some media queries:

const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      background: myDarkTheme.background
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

☝️ I typically use JSS for styling my web apps. It provides style isolation, nested styling, etc. out of the box, and I am particularly fond of it alongside JSX (so everything is in JS).

 

Things become more complicated when you consider the fact that you cannot rely on system settings for dark mode as some operating systems do not have such a setting.

A solution I often use myself is adding a button that allows the user to override dark mode settings. By default, the setting is read from system (media query), but if the user overrides that, then the preference will be stored in local storage and applied as a CSS class:

const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      background: myDarkTheme.background
    },
    'html.--dark &': {
      background: myDarkTheme.background
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

☝️ This code is actually not complete: if a user whose OS is in dark mode changes preference to light mode, this styling rule would still display in dark mode for them. To fix that, we can add a separate class indicating whether OS dark mode is being overridden or not (regardless of its value):

const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      'html:not(.--dark-mode-override) &': {
        background: myDarkTheme.background
      }
    },
    'html.--dark &': {
      background: myDarkTheme.background
    }
  }
}
Enter fullscreen mode Exit fullscreen mode

 

This is a lot of boilerplate for a simple style. The solution? Well if my styles where functions of a theme instead of plain values, i.e.:

const myStyles = theme => {
  button: {
    background: theme.background
  }
}
Enter fullscreen mode Exit fullscreen mode

Then I could run the styles for the two themes I have (light mode and dark mode), and insert additional CSS rules for properties that differ automatically:

const stylesInLight = myStyles(myTheme)
const stylesInDark = myStyles(myDarkTheme)

const D = diff(stylesInLight, stylesInDark)
createAndInjectRules(D)
Enter fullscreen mode Exit fullscreen mode

 

themed-jss

What I described here is basically what themed-jss does. It allows defining styles in terms of functions of themes, and based on that it automatically creates additional styles for dark mode.

Here is a React example:

// my-btn.style.js

import { style } from 'themed-jss'

export default style(theme => ({
  btn: {
    background: theme.primary,
    color: theme.background,
    border: 'none',
    borderRadius: 3
  }
})
Enter fullscreen mode Exit fullscreen mode
// my-btn.jsx

import React from 'react'
import { useThemedStyle } from 'themed-jss/react'

import styles from './my-btn.style'

export default () => {
  const { btn } = useThemedStyle(styles)

  return (
    <button className={btn}>Click ME!</button>
  )
}
Enter fullscreen mode Exit fullscreen mode

This component can be used like this:

// app.jsx

import { theme } from 'themed-jss'
import { Themed } from 'themed-jss/react'

import MyBtn from './my-btn'

const myTheme = theme(
  {
    primary: 'green',
    background: 'white',
    text: 'black'
  },
  // --> dark mode overrides:
  {
    background: 'black',
    text: 'white'
  }
)

export default () => (
  <Themed theme={myTheme}>
    <MyBtn/>
  </Themed> 
)
Enter fullscreen mode Exit fullscreen mode

 

For manual control of dark mode, I would simply need to import DarkMode from themed-jss/dark-mode and initialize it:

// app.jsx

import { DarkMode } from 'themed-jss/dark-mode'

DarkMode.initialize()

// ...
Enter fullscreen mode Exit fullscreen mode

And now I can have <MyBtn/> to actually switch the dark mode:

// my-btn.jsx

import { DarkMode } from 'themed-jss/dark-mode'

// ...

export default () => {
  const { btn } = useThemedStyle(styles)

  return (
    <button
      className={btn}
      onClick={() => DarkMode.toggle()}>
      Click ME!
    </button>
  )
}
Enter fullscreen mode Exit fullscreen mode

👉 Try it out for yourself!

 


 

Anyways, I hope you would find themed-jss useful as well. You can find the complete docs in the readme, and comment here (or create an issue on GitHub, etc) if you've got any further questions. This is a pretty young tool, so any feedback is much appreciated!

Top comments (0)