戴兜

戴兜

Coding the world.
github
bilibili
twitter

Implementing theme color switching with SCSS+WindiCSS

Recently, I have been working on creating my own homepage (which is also a blog), and I have implemented a theme color switching feature. Every time the page is loaded, a random color scheme is selected to make the page more dynamic, like the example below:

image

How was this implemented? Let's start with custom colors.

WindiCSS Custom Colors#

Define a fixed color#

// windi.config.js

export default defineConfig({
  theme: {
    extend: {
      colors: {
        primary: "#2196f3",
      },
    },
  },
})

This defines a color called primary, which can be used normally afterwards (e.g., bg-primary / text-primary).

Of course, you can not only pass a string, but also use an object to define a set of colors (e.g., bg-primary-light).

// windi.config.js

export default defineConfig({
  theme: {
    extend: {
      colors: {
        primary: {
          extralight: "#d3eafd",
          light: "#b2dafb",
          medium: "#6ebbf7",
          DEFAULT: "#2196f3"
        },
      },
    },
  },
})

Use CSS variables#

To make the colors adjustable, using CSS variables would be much more convenient, and WindiCSS supports it as well:

:root {
  --color-primary-extralight: #d3eafd;
  --color-primary-light: #b2dafb;
  --color-primary-medium: #6ebbf7;
  --color-primary: #2196f3;
  --color-primary-dark: #27415b;
}
// windi.config.js

export default defineConfig({
  theme: {
    extend: {
      colors: {
        primary: {
          extralight: 'var(--color-primary-extralight)',
          light: 'var(--color-primary-light)',
          medium: 'var(--color-primary-medium)',
          DEFAULT: 'var(--color-primary)',
          dark: 'var(--color-primary-dark)',
        },
      },
    },
  },
})

This allows basic usage of CSS variables in WindiCSS. However, there is a small issue:

WindiCSS supports setting transparency for colors, for example, bg-gray-800/80 or bg-gray-800 bg-opacity-80. The above configuration will cause this syntax to fail (losing transparency). Therefore, we need to change the format of CSS variables. At the same time, we need a higher-order utility function to wrap the variables:

:root {
  --color-primary-extralight: 211 234 253;
  --color-primary-light: 178 218 251;
  --color-primary-medium: 110 187 247;
  --color-primary: 33 150 243;
  --color-primary-dark: 39 65 91;
}
// windi.config.js

function withOpacityValue(variable) {
  return val => {
    if (val.opacityValue === undefined) {
      return `rgb(var(${variable}))`
    }
    return `rgb(var(${variable}) / ${val.opacityValue})`
  }
}

export default defineConfig({
  theme: {
    extend: {
      colors: {
        primary: {
          extralight: withOpacityValue('--color-primary-extralight'),
          light: withOpacityValue('--color-primary-light'),
          medium: withOpacityValue('--color-primary-medium'),
          DEFAULT: withOpacityValue('--color-primary'),
          dark: withOpacityValue('--color-primary-dark'),
        },
      },
    },
  },
})

This way, whenever the primary color is used, WindiCSS will call the function to generate styles, and support transparency syntax by judging the opacityValue.

SCSS Generating CSS Variables#

Obviously, it is not practical to manually specify color values for variations like light and extralight, and it is also difficult to maintain since we now need to represent colors with three numbers in RGB format without syntax highlighting or intuitiveness.

This is where SCSS comes in handy! SCSS provides basic CSS data types, conditional and iteration syntax, as well as a wealth of utility functions (such as red(), blue(), green(), etc. for channel separation, mix() for color blending).

First, let's implement a utility function that converts the input hexadecimal color into the form of three RGB numbers:

@function getColorValue($color) {
  @return #{red($color)} #{green($color)} #{blue($color)};
}

/* getColorValue(#2196f3) -> 33 150 243 */

My expectation is that as long as I provide a base color for primary, SCSS will generate all the color variations such as light and extralight for me. I use the mix method to achieve this:

@mixin spread-theme-map($map: ()) {
  @each $key, $value in $map {
    #{"--"+$key}: $value;
  }
}

@function theme-primary-map($primary-color: #2196f3) {
  @return (
    color-primary-dark: getColorValue(mix($primary-color, black, 30%)),
    color-primary: getColorValue($primary-color),
    color-primary-medium: getColorValue(mix($primary-color, white, 70%)),
    color-primary-light: getColorValue(mix($primary-color, white, 35%)),
    color-primary-extralight: getColorValue(mix($primary-color, white, 15%))
  );
}

/* spread-theme-map(theme-primary-map(#2196f3)) */

This way, corresponding color properties can be generated for a specific color. Next, we just need to define an array and put the desired theme colors into it, then run a loop (I randomly picked a few pleasing colors from the Material Design documentation):

$themeColorList: (
  #2196f3,
  #f44336,
  #9c27b0,
  #4caf50,
  #3f51b5,
  #795548,
  #607d8b,
  #009688
);

@for $i from 1 through length($themeColorList) {
  $color: nth($themeColorList, $i);
  .theme-#{$i} {
    @include spread-theme-map(theme-primary-map($color));
  }
}

In VSCode, it looks like this:

image

Much more comfortable, isn't it?

The remaining work should be crossed out#

If you want to modify the theme color, just add the corresponding class name to the root element (html or body). There are many ways to achieve this, but since I am using Nuxt.js, here is my solution:

const randomThemeColorIndex = useState('randomThemeColorIndex', () =>
  Math.floor(Math.random() * themeColorList.length) + 1
)

useHead({
  bodyAttrs: {
    class: 'theme-' + randomThemeColorIndex.value,
  }
})
Loading...
Ownership of this post data is guaranteed by blockchain and smart contracts to the creator alone.