Perfect solution for theme switching based on Css Variable (recommended)

Perfect solution for theme switching based on Css Variable (recommended)

When receiving this requirement, Baidu found many solutions for theme switching in the industry, including css link replacement, className change, less.modifyVars, css in js, etc., but each solution sounded tiring and expensive. Is there a solution that has low code intrusion, is easy to use and maintain? Of course there is. To be more precise, CSS itself supports it.

Css3 Variable

Define a global color variable. When you change the value of this variable, all elements that reference this variable in the page will change. Very simple, isn’t it?

// base.less
:root {
  --primary: green;
  --warning: yellow;
  --info: white;
  --danger: red;
}

// var.less
@primary: var(--primary)
@danger: var(--danger)
@info: var(--info)

// page.less
.header {
  background-color: @primary;
  color: @info;
}
.content {
  border: 1px solid @danger;
}
//change.js
function changeTheme(themeObj) {
  const vars = Object.keys(themeObj).map(key => `--${key}:${themeObj[key]}`).join(';')
  document.documentElement.setAttribute('style', vars)
}

End of this article

Fuck, it doesn't support IE ! ! Will it still be compatible with IE in 2020? Yes, it has to be compatible with IE.

css vars ponyfill

Yes, there is a polyfill that is compatible with IE: css-vars-ponyfill. This is how it handles IE

+-------------------------+
| Get the content of the style tag in the page |
| Request external link css content |
+-------------------------+
|
|
v
+-------------------------+ Yes +-------------------------+
| Does the content contain var() | ----> | Marked as src |
+-------------------------+ +-------------------------+
| |
| No |
v v
+-------------------------+ +-------------------------+
| Mark as skip | | Replace var(*) with variable value, |
| | | Add new style tag to head |
+-------------------------+ +-------------------------+

The effect is probably like this

Simple and crude yet elegant, it will not be processed in browsers that support css var, so there is no need to worry about performance issues ( it is IE's problem, not my problem)

). Let's modify the code

//store/theme.js
import cssVars from 'css-vars-ponyfill'

export default {
  state: {
    'primary': 'green',
    'danger': 'white'
  },
  mutations:
    UPDATE_THEME(state, payload) {
      const variables = {}
      Object.assign(state, payload)
      Object.keys(state).forEach((key) => {
        variables[`--${key}`] = state[key]
      })
      cssVars({
        variables
      })
    }
  },
  actions: {
    changeTheme({ commit }, theme = {}) {
      commit('UPDATE_THEME', theme)
    }
  }
}

// router.js
// Because the page after the route jump will load new CSS resources on demand, re-convert const convertedPages = new Set()
router.afterEach((to) => {
  if (convertedPages.has(to.path)) return
  convertedPages.add(to.path)
  context.store.dispatch('theme/changeTheme')
})

SSR project flash screen problem optimization

In SSR projects, you may see this in IE using the above solution

Because css-vars-ponyfill

It relies on DOM elements to achieve conversion and cannot be used in node, so there is a style gap between the server directly outputting the unconverted CSS code and the client loading the JS file to convert the CSS.

+- - - - - - - - - - - - - - - - - - - - - +
'Style window period: '
' '
+----------+ ' +----------------+ +------------+ ' +-------------+
| Initiate request | --> ' | SSR direct output page | --> | Load js dependency | ' --> | Replace css variables |
+----------+ ' +----------------+ +------------+ ' +-------------+
' '
+- - - - - - - - - - - - - - - - - - - - - +

Solving this problem is also very simple. Just add a compatible writing method to each place where css var is used.

@_primary: red
@primary: var(--primary)

:root{
  --primary: @_primary
}

.theme {
  color: @primary;
}

//Change to .theme {
  color: @_primary;
  color: @primary;
}

On browsers that do not support css var, the default color red will be rendered, and ponyfill will replace the style override after js is loaded.

Webpack plugin development

Manually adding compatible writing in each place is both laborious and difficult to maintain. At this time, we need to understand some knowledge related to the webpack life cycle and plug-in development. We can write a webpack plug-in by hand and add a loader to all css modules in the hooks of normalModuleLoader ( v5 version is deprecated, use NormalModule.getCompilationHooks(compilation).loader ) to handle compatible code.

The author's project uses less. Note that the execution order of loaders in webpack is similar to the first-in-last-out order of a stack , so I need to add the conversion loader before the less-loader to ensure that we are dealing with the compiled css var writing rather than the less variable.

// plugin.js
export default class HackCss {
  constructor (theme = {}) {
    this.themeVars = theme
  }

  apply(compiler) {
        compiler.hooks.thisCompilation.tap('HackCss', (compilation) => {
          compilation.hooks.normalModuleLoader.tap(
            'HackCss',
            (_, moduleContext) => {
              if (/\.vue\?vue&type=style/.test(moduleContext.userRequest)) {
                // There will be 2 compilers for ssr project isomorphism. If there is a loader in the module, it will not be added if (hasLoader(moduleContext.loaders, 'hackcss-loader.js')) {
                  return
                }

                let lessLoaderIndex = 0
                // The project uses less, find the location of less-loadermoduleContext.loaders.forEach((loader, index) => {
                  if (/less-loader/.test(loader.loader)) {
                    lessLoaderIndex = index
                  }
                })
  
                moduleContext.loaders.splice(lessLoaderIndex, 0, {
                  loader: path.resolve(__dirname, 'hackcss-loader.js'),
                  options: this.themeVars
                })
              }
            }
          )
        })
      }
    })
}

// loader.js
const { getOptions } = require('loader-utils')

module.exports = function(source) {
  if (/module\.exports/.test(source)) return source
  const theme = getOptions(this) || {}
  return source.replace(
    /\n(.+)?var\(--(.+)?\)(.+)?;/g,
    (content, before, name, after = '') => {
      const [key, indent] = before.split(':')
      const add = after.split(';')[0]
      return `\n${key}:${indent}${theme[name]}${after}${add};${content}`
    }
  )
}

At this point, we can switch themes happily and freely.

postscript

It is more interesting to absorb new knowledge by learning how to be “too lazy to write more code”. I hope this article can help you.

This is the end of this article about the perfect solution for theme switching based on Css Variable (recommended). For more relevant css Variable theme switching content, please search for previous articles on 123WORDPRESS.COM or continue to browse the related articles below. I hope everyone will support 123WORDPRESS.COM in the future!

<<:  Analysis of several reasons why Iframe should be used less

>>:  Detailed explanation of obtaining, assigning, and registering radio values ​​in HTML

Recommend

Several solutions for CSS record text icon alignment

It is very common to see images and text displaye...

Three uses and differences of MySQL not equal

Judgment symbols are often used in MySQL, and not...

Sample code for achieving small triangle border effect with pure CSS3+DIV

The specific code is as follows: The html code is...

Introduction to TypeScript basic types

Table of contents 1. Basic types 2. Object Type 2...

Detailed explanation of the principle and usage of MySQL views

This article uses examples to illustrate the prin...

Shtml Concise Tutorial

Shtml and asp are similar. In files named shtml, s...

How to configure MySQL8 in Nacos

1. Create the MySQL database nacos_config 2. Sele...

Mysql modify stored procedure related permissions issue

When using MySQL database, you often encounter su...

Solutions to invalid is Null segment judgment and IFNULL() failure in MySql

MySql Null field judgment and IFNULL failure proc...

Solve the MySQL login 1045 problem under centos

Since the entire application needs to be deployed...

A few things you need to know about responsive layout

1. Introduction Responsive Web design allows a we...

Detailed steps for installing and configuring mysql 5.6.21

1. Overview MySQL version: 5.6.21 Download addres...

Summary of methods to check whether the port is open in Linux

Method 1: Use lsof command We can use the lsof co...