DEV Community

qinmu
qinmu

Posted on

Implement Dark Mode in Vue/React UI Component Library

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

Image description

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

For the color that will change with the mode, we name Themeprefix.

// 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)
Enter fullscreen mode Exit fullscreen mode

Image description

Image description
For the colors that will not change with the dark or light mode, we name Always as the prefix of the Token.

// always/index.styl
$AlwaysWhiteFill = rgba(255, 255, 255, 0.99)
Enter fullscreen mode Exit fullscreen mode

Image description

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.

$Divider: #ddd;

.main-header {
  border: 1px solid $Divider;
}
Enter fullscreen mode Exit fullscreen mode

For example, the code would be compiled into:

.main-header {
  border: 1px solid #ddd;
}
Enter fullscreen mode Exit fullscreen mode

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.

:root {
  --Divider: #ddd;
}

.main-header {
  border: 1px solid var(--Divider);
}
Enter fullscreen mode Exit fullscreen mode

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:

:root {
  --width: 30%;
}
@media (max-width: 450px) {
  :root {
    --width: 60%;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

// dark mode
$DarkDivider: #eee;

// light mode
$LightDivider: #ddd;

// component example
.divider {
    border: 1px solid $LightDivider;
  &.dark {
    border: 1px solid $DarkDivider;
  }
}
Enter fullscreen mode Exit fullscreen mode

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.

// light style
--Divider: #ddd;

// Dark style
--Divider: #eee;

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

:root, .dark-mode {
  --Divider: #eee;
}
Enter fullscreen mode Exit fullscreen mode

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.

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

// demo.vue
textStyle.color = colors.ThemeGray
Enter fullscreen mode Exit fullscreen mode

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, or recommend your users to use ready-made solution like @nuxtjs/color-mode
Let’s see how they are working under the hood.

Vant Solution—ConfigProvider

To enable ConfigProvider, you should register component globally.

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

const app = createApp();
app.use(ConfigProvider);
Enter fullscreen mode Exit fullscreen mode

Then you can enable dark mode like this:

<van-config-provider theme="dark">
  ...
</van-config-provider>
Enter fullscreen mode Exit fullscreen mode

ConfigProvider is essentially a component. It will watch the theme props, and dynamically add or remove class to document.documentElement.

Image description

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

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.clientand plugin.server)and one script. Let’s see how they are working together.

Image description
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
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-schemeto users?

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}
Enter fullscreen mode Exit fullscreen mode

When we want to customize the dark mode color for certain component, we simply use:

.reds-button-new.outlined
  +prefers-color-scheme(dark)
    background-color red
Enter fullscreen mode Exit fullscreen mode

This will greatly improve the code readability.

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

Top comments (0)