Auto Dark Mode w/ JSS
Tags: react, css, javascript, showdev
Published: 2021-01-05 14:45:33

Recently I've built a little library, called [`themed-jss`](https://github.com/loreanvictor/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.

&nbsp;<br>

# 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`:

```ts
const myTheme = {
  buttons: 'blue',
  text: 'black',
  background: 'white'
}
```

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.

&nbsp;<br>

# Darkmoding

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

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

const myDarkTheme = {
  ...myTheme,
  background: 'black',
  color: 'white'
}
```

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:

```ts
const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      background: myDarkTheme.background
    }
  }
}
```
> ☝️ I typically use [JSS](https://cssinjs.org/) 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).

&nbsp;

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:

```ts
const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      background: myDarkTheme.background
    },
    'html.--dark &': {
      background: myDarkTheme.background
    }
  }
}
```
☝️ 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):
```ts
const myStyles = {
  button: {
    background: myTheme.background,
    '@media (prefers-color-scheme)': {
      'html:not(.--dark-mode-override) &': {
        background: myDarkTheme.background
      }
    },
    'html.--dark &': {
      background: myDarkTheme.background
    }
  }
}
```

&nbsp;<br>

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

```ts
const myStyles = theme => {
  button: {
    background: theme.background
  }
}
```

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:

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

const D = diff(stylesInLight, stylesInDark)
createAndInjectRules(D)
```

&nbsp;<br>

# `themed-jss`

What I described here is basically what [`themed-jss`](https://github.com/loreanvictor/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:

```tsx
// my-btn.style.js

import { style } from 'themed-jss'

export default style(theme => ({
  btn: {
    background: theme.primary,
    color: theme.background,
    border: 'none',
    borderRadius: 3
  }
})
```
```jsx
// 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>
  )
}
```

This component can be used like this:

```jsx
// 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> 
)
```

&nbsp;<br>

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

```jsx
// app.jsx

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

DarkMode.initialize()

// ...
```

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

```jsx
// 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>
  )
}
```

[👉 Try it out for yourself!](https://stackblitz.com/edit/react-themed-jss?file=src%2FApp.js)

&nbsp;<br>

---

&nbsp;<br>

Anyways, I hope you would find `themed-jss` useful as well. You can find the complete docs [in the readme](https://github.com/loreanvictor/themed-jss), 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!