Implement Dark Mode in Vue/React UI Component Library
Published: 2022-12-29 13:00:16

Unlike other blogs only telling you how to implement Dark Mode using React Hooks to toggle theme, this blog will explain how Dark Mode will be implemented across **the whole Design to Code stages from a bottom-top side**. The stages include:

- Design token and style variable naming conventions
- Css variables and Javascript color variables
- Global dark mode implementation
- Customize component-level dark mode color

## Design token and style variable naming conventions

Dark and Light mode share the same token name. Let's take the ProgressIndicator component as an example. The color of the circle changes with the Dark/Light mode, but they share the same Design Token--`Fill`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/fllmcphqoivmkfcd60us.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0kbgx2d3i3s3a1f5daja.png)

Speaking of the css variable naming convention, we add prefix to differentiate them.

For the color that will change with the mode, we name `Theme`prefix.

```scss
// dark/index.styl
$Fill = rgba(255, 255, 255, 0.2)
$SecondaryFill = rgba(255, 255, 255, 0.1)

// light/index.styl
$Fill = rgba(0, 0, 0, 0.08)
$SecondaryFill = rgba(0, 0, 0, 0.04)

// theme/index.styl
$ThemeFill = var(--ThemeFill, $Fill)
$ThemeSecondaryFill = var(--ThemeSecondaryFill, $SecondaryFill)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wely3tp94hriohovkezx.png)

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uqtcyttrpg8emsj3sd87.png)
For the colors that will not change with the dark or light mode, we name Always as the prefix of the Token.

```scss
// always/index.styl
$AlwaysWhiteFill = rgba(255, 255, 255, 0.99)
```

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/3j3sg6e3utqemqdf915r.png)

## CSS variables and Javascript color variables

As we've already provided stylus variables, do we need css variables?

The answer is YES.

Stylus variables are a part of CSS preprocessors,  and need to be compiled into CSS to be understood by the browser.

```scss
$Divider: #ddd;

.main-header {
  border: 1px solid $Divider;
}
```
For example, the code would be compiled into:

```scss
.main-header {
  border: 1px solid #ddd;
}
```
Once the code compiles, the variables are gone.

CSS variables, however, is natively supported within CSS. It **doesn’t need to be compiled**, and can be directly used.

```scss
:root {
  --Divider: #ddd;
}

.main-header {
  border: 1px solid var(--Divider);
}
```
So what is the biggest advantage of CSS variables vs preprocessor variables? 

Through CSS variables, styles are changed **in runtime instead of compilation time**. This will bring following benefits:

**BENEFIT 1**:  Enable us to **reset style in runtime,** which is impossible with preprocessor variables.

For example, in grid layout, media query could change the variables:

```scss
:root {
  --width: 30%;
}
@media (max-width: 450px) {
  :root {
    --width: 60%;
  }
}
```
Another example is that if you use preprocessor variables to switch UI mode, you have to provide two different preprocessor variables to the same value, and you have to write many duplicated code to tell the browser to use the right color, which enlarges the code size and make it hard to manage styles.  e.g.

```scss
// dark mode
$DarkDivider: #eee;

// light mode
$LightDivider: #ddd;

// component example
.divider {
	border: 1px solid $LightDivider;
  &.dark {
    border: 1px solid $DarkDivider;
  }
}
```
However, if you use css variables, you don’t have to provide two different variables for the same value. And you don’t have to write duplicated code. When you switch class name, In light mode, browser will use `#ddd`, and in dark mode, browser will use `#eee`.

```scss
// light style
--Divider: #ddd;

// Dark style
--Divider: #eee;

// index.css
:root, .light-mode {
  --Divider: #ddd;
}

:root, .dark-mode {
  --Divider: #eee;
}
```
BENEFIT 2: You could manipulate them in JavaScript.  In component library, you could only allow users to pass Color Token instead of pass any rgba or hex values, which brings chaos and hard to manage styles.

```scss
// colors.js
const ThemeGray = `var(--ThemeGray)`

// demo.vue
textStyle.color = colors.ThemeGray
```
Despite differences between preprocessor variables and less variables, they can work together. Let’s have a look! 

## Global dark mode implementation

In most time, we apply theme changes globally.  You could either implement in the component library like [Vant](https://vant-contrib.gitee.io/vant/#/en-US/config-provider), or recommend your users to use ready-made solution like [`@nuxtjs/color-mode`](https://github.com/nuxt-modules/color-mode)
Let’s see how they are working under the hood.

### Vant Solution—ConfigProvider

To enable ConfigProvider, you should register component globally.

```javascript
import { createApp } from 'vue';
import { ConfigProvider } from 'vant';

const app = createApp();
app.use(ConfigProvider);
```
Then you can enable dark mode like this:

```
<van-config-provider theme="dark">
  ...
</van-config-provider>
```
ConfigProvider is essentially a component. It will watch the theme props, and dynamically add or remove class to `document.documentElement`.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/mfzc2i4uvylldboeozz7.png)

Vant provides css variables for dark theme. When you add class van-them-black, it will use these css variables to replace light theme css variables.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/svpa0kf7d0eh22btz69x.png)

### Nuxt Solution—ColorMode Module

Nuxt module enables us to share custom solutions as npm packages without adding unnecessary boilerplate.

`@nuxtjs/color-mode` mainly provide two plugins (`plugin.client`and `plugin.server`）and one script. Let’s see how they are working together.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ndtwxj09dp2wnevg13cy.png)
To be mentioned, on the server side, it will use preference options color as initial colorMode, and the colorMode class has not been added to the template, and the css variables are loaded asynchronously. So like below, even you set the app to be dark mode, you will see the preview is still in light mode.

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9rmd9ebfdd940agymv76.png)
So when will the dark mode class be added? 

On the client side, the browser begins to execute script. It gets the exact colorMode from localStorage or system color or the forced set color. Then it will set className to document.documentElement, which will let the browser know to use the relevant mode css variables. 

Then, client plugin begins to watch color mode changes from user and watch colorMode changes from the router options.

### Customize component-level dark mode color

Instead of using the dark mode color provided by the component library, sometimes we want to customize the dark mode color for certain component. 

The easiest way to do this is to wrap dark mode class outside the component class. You can directly use `dark-mode` class, but it would be a little bit disordered, as dark-mode class is different from other classes.

Why don’t we provide a stylus function `prefers-color-scheme`to users?

```scss
prefers-color-scheme(scheme)
  themes = light dark
  error('invalid color scheme: <light | dark>') unless scheme in themes

  if scheme == light
    &, .light-mode &
      {block}
  else if scheme == dark
    .dark-mode &
      {block}
```
When we want to customize the dark mode color for certain component, we simply use:

```scss
.reds-button-new.outlined
  +prefers-color-scheme(dark)
    background-color red
```

This will greatly improve the code readability.

That’s all. Feel free to ask me any questions about dark mode.