How to develop Light And Dark Mode using Tailwind and CSS Variables

This post will show you how to develop light and dark mode using Tailwind without having to hardcode 'dark' variant throughout your code utilizing CSS Variables and Material Design concept surface tones based on elevation. And in turn, you also get flexibility to change color without having to resort to find and replace!

Tailwind light to dark

Content

Dark mode using dark modifier

Tailwind documentation prescribes use of 'dark' variant to style your site differently when dark mode is enabled:

<div class="bg-white dark:bg-slate-800 rounded-lg px-6 py-8 ring-1 ring-slate-900/5 shadow-xl">
  <h3 class="text-slate-900 dark:text-white mt-5 text-base font-medium tracking-tight">Hello World</h3>
</div>
<div class="bg-white dark:bg-slate-800 rounded-lg px-6 py-8 ring-1 ring-slate-900/5 shadow-xl">
  <h3 class="text-slate-900 dark:text-white mt-5 text-base font-medium tracking-tight">Hello World</h3>
</div>

dark: variant has been used to change background color from bg-white to bg-slate-800 and change text color from text-slate-900 to text-white.

If you follow the documentation, you will have to design every component for both the light and the dark mode and test each one of them. You will also be setting yourself up for lot of maintenance down the line as your app matures and you need more options and/or have to change colors (e.g. if we wanted to swap out dark:bg-slate-800 for dark:bg-gray-900).

Dark mode without modifier

But there is a easier way to achieve light and dark mode using standard CSS. Secret is to use CSS variables and combine it with the concept of Elevation and Surface tones as outlined in Material Design system.

Lets take the background color. Using concept of surface color changing as the elevation increases, we can design them as follows

ElevationLight ModeDark Mode
Surfacebg-slate-50bg-slate-900
Surface + 1bg-slate-100bg-slate-800
surface + 2bg-slate-200bg-slate-700
surface + 3bg-slate-300bg-slate-600
surface + 4bg-slate-400bg-slate-500
surface + 5bg-slate-500bg-slate-400

Noticed the change? Background colors for the dark mode are reversed! Of course you should customize above based on your design system, but this is a good enough to showcase the approach. With above in place, when you design your component, instead of specifying separate background for light and dark mode, you just specify it once

Before:

<div class="bg-white dark:bg-slate-800 text-slate-900 dark:text-white">Hello</div>
<div class="bg-white dark:bg-slate-800 text-slate-900 dark:text-white">Hello</div>

After:

<div class="bg-surface text-primary">Hello</div>
<div class="bg-surface text-primary">Hello</div>

Now depending on the light or dark mode, background will auto change thanks to browser and use of CSS Variables!

In addition to background, I normally define CSS Variables for following main areas of the app that need to change color based on light and dark mode:

  • Text Colors
  • Border Colors
  • Input Colors

With these CSS variables, you can go about designing components and not have to worry about light and dark mode, it just works!

Example

Lets see how to do it, I will use standard tailwind css slate color:

:root,
.light {
  --background-color-50: 248 250 252;
  --background-color-100: 241 245 249;
  --background-color-200: 226 232 240;
  --background-color-300: 203 213 225;
  --background-color-400: 148 163 184;
  --background-color-500: 100 116 139;
  --background-color-600: 71 85 105;
  --background-color-700: 51 65 85;
  --background-color-800: 30 41 59;
  --background-color-900: 15 23 42;
}
:root,
.light {
  --background-color-50: 248 250 252;
  --background-color-100: 241 245 249;
  --background-color-200: 226 232 240;
  --background-color-300: 203 213 225;
  --background-color-400: 148 163 184;
  --background-color-500: 100 116 139;
  --background-color-600: 71 85 105;
  --background-color-700: 51 65 85;
  --background-color-800: 30 41 59;
  --background-color-900: 15 23 42;
}

And for dark mode

.dark {
  --background-color-50: 15 23 42;
  --background-color-100: 30 41 59;
  --background-color-200: 51 65 85;
  --background-color-300: 71 85 105;
  --background-color-400: 100 116 139;
  --background-color-500: 148 163 184;
  --background-color-600: 203 213 225;
  --background-color-700: 226 232 240;
  --background-color-800: 241 245 249;
  --background-color-900: 248 250 252;
}
.dark {
  --background-color-50: 15 23 42;
  --background-color-100: 30 41 59;
  --background-color-200: 51 65 85;
  --background-color-300: 71 85 105;
  --background-color-400: 100 116 139;
  --background-color-500: 148 163 184;
  --background-color-600: 203 213 225;
  --background-color-700: 226 232 240;
  --background-color-800: 241 245 249;
  --background-color-900: 248 250 252;
}

Note that the background colors for the dark mode have been reversed!

Lets make changes to our tailwind.config.js so that we can use above colors when designing components

module.exports = {
...
  theme: {
    extend: {
      textColor: {
        primary: "rgb(var(--background-color-900) / <alpha-value>)",
        secondary: "rgb(var(--background-color-600) / <alpha-value>)",
      },
      backgroundColor: {
        surface: "rgb(var(--background-color-50) / <alpha-value>)",
        surface1: "rgb(var(--background-color-100) / <alpha-value>)",
        surface2: "rgb(var(--background-color-200) / <alpha-value>)",
        surface3: "rgb(var(--background-color-300) / <alpha-value>)",
        surface4: "rgb(var(--background-color-400) / <alpha-value>)",
        surface5: "rgb(var(--background-color-500) / <alpha-value>)",
      },
    }
  }
}
module.exports = {
...
  theme: {
    extend: {
      textColor: {
        primary: "rgb(var(--background-color-900) / <alpha-value>)",
        secondary: "rgb(var(--background-color-600) / <alpha-value>)",
      },
      backgroundColor: {
        surface: "rgb(var(--background-color-50) / <alpha-value>)",
        surface1: "rgb(var(--background-color-100) / <alpha-value>)",
        surface2: "rgb(var(--background-color-200) / <alpha-value>)",
        surface3: "rgb(var(--background-color-300) / <alpha-value>)",
        surface4: "rgb(var(--background-color-400) / <alpha-value>)",
        surface5: "rgb(var(--background-color-500) / <alpha-value>)",
      },
    }
  }
}

With above in place we can just use bg-surface for the base surface in your component. Any surface above that will use bg-surface1 and so on. This works surprisingly well for both light and dark mode and gives users a perception of depth.

<div class="bg-surface rounded-lg px-6 py-8">
  <h3 class="bg-surface1 text-primary mt-5 text-base font-medium tracking-tight">Hello World</h3>
</div>
<div class="bg-surface rounded-lg px-6 py-8">
  <h3 class="bg-surface1 text-primary mt-5 text-base font-medium tracking-tight">Hello World</h3>
</div>

Tailwind Light Dark Auto

You can always tweak and override where you want to customize, but out of the box this system provides a great looking light and dark mode experience for users. And when you want to make changes, it is easy, just update the CSS variables and you have a new set colors!

Next Steps

Hopefully you now have a good idea of how to develop components that support light and dark with Tailwind and at same time make it easy to maintain.

Find Source code with a ready to run project @ GitHub

RemixFast

RemixFast Stater includes components that are designed using Tailwind and utilize above approach to make it easy to change and maintain.

References

By using RemixFast, you agree to our Cookie Policy.