Recently my company needed to add multiple skin functions to multiple WebApps (about 20 +) . The default was white skin, so we started with dark mode to gradually achieve multiple skin functions. This article is a record of the implementation ideas.
Solution for skin-changing
css variables
css variables is the Web standard that implements support for dark patterns,
the following code queries through the CSS Media, the simplest implementation.
:root {
color-scheme: light dark;
background: white;
color: black;
}
@media (prefers-color-scheme: dark) {
:root {
background: black;
color: white;
}
}
Use CSS variables if you have a lot of colors
:root {
color-scheme: light dark;
--nav-bg-color: #F7F7F7;
--content-bg-color: #FFFFFF;
--font-color: rgba(0,0,0,.9);
}
@media (prefers-color-scheme: dark) {
:root {
--nav-bg-color: #2F2F2F;
--content-bg-color: #2C2C2C;
--font-color: rgba(255, 255, 255, .8);
}
}
:root {
color: var(--font-color)
}
.header {
background-color: var(--nav-bg-color);
}
.content {
background-color: var(--content-bg-color);
}
Advantages: the least amount of code, easy to implement;
The downside: There’s a Cross-browser, which is supported by Edge16 + ; older projects are implemented, and CSS needs to be refactored, so it’s not going to work for us, and if it’s a new WebApp, I wouldn’t hesitate to use it.
Online compilation use less.js
The most typical example of this scenario is the https://antdtheme.com/ , via the less modifyVars
method
Enables run-time changes to fewer variables. When called with a new value, fewer files are recompiled without reloading.
<script src="less.js"></script>
<script>
less.modifyVars({ '@text-color': '#fff', '@bg-color': '#000' });
</script>
If there are too many color variables to change, or too many style files, it will cause Cottonwood to switch.
Build multiple CSS
Of course, you can also manually build 2 CSS styles
var less = require("less");
var fs = require("fs");
fs.readFile("./index.less", "utf-8", (err, str) => {
less.render(
str,
{
paths: [".", "./components"], // the search path for the@import directive
compress: true,
modifyVars: {
"@text-color": "#fff",
"@bg-color": "#000",
},
},
function (e, output) {
console.log(output.css);
}
);
});
So you can skin it by dynamically inserting CSS
function changeTheme(theme) {
const styleCss = document.querySelector("#styleCss");
if (styleCss) {
styleCss.href = `/assets/css/${theme}.css`;
} else {
const head = document.getElementsByTagName("head")[0];
const link = document.createElement("link");
link.id = "styleCss";
link.type = "text/css";
link.rel = "stylesheet";
link.dataset.type = "theme";
link.href = `/assets/css/${theme}.css`;
head.appendChild(link);
}
localStorage.setItem("theme", theme);
}
One problem with this approach is that it causes the entire page to reorder when you click to switch, so we need to separate out the color-only style files. From this point of view, we are exposed to PostCSS.
PostCSS
The PostCSS core contains a parser that generates a CSS AST (Abstract Syntax Tree), which is a representation of a node tree that parses strings of CSS. When we change something inside the CSS Abstract Syntax Tree, PostCSS will still represent it as a root node but stringify the syntax tree back into a CSS string.
The core process is ** Parse->Transform--> Generate ** Is it like Babel ?
Everyone knows that https://astexplorer.net/ is a site that can be used to write Babel plugins, but have you used any other parsers? Select CSS and PostCSS here so you can parse your CSS into your CSS AST (abstract syntax tree) .
Purpose
Currently I have one less style and two color variables, I need to generate the following style:
So I can add and remove the ‘dark’ class in the HTML root node to do this.
Some of you may be asking, Suddenly, why is it Less? Can PostCSS parse Less? The answer is no.
At the moment, I'am sure your webapp is based on Webpack
.
module: {
rules:[
//...
{
test: /\.less$/i,
use: ['style-loader', 'css-loader', 'postcss-loader', 'less-loader'],
},
//...
]
}
Webpacks loader's order of execution is from right to left, Less pass by less-loader,It becomes CSS
Start writing a PostCSS plugin
We can use postcss-plugin-boilerplate , This scaffolding to create a postcss-plugin ,It also has jest unit tests configured。You can create one postcss-plugin project with a few simple commands。
Of course we can just create a JS file in the project root directory
// test-plugin.js
var postcss = require("postcss");
module.exports = postcss.plugin("pluginname", function (opts) {
opts = opts || {}; // plugin 参数
return function (root, result) {
// Transform the CSS AST
};
});
And then, after that, Just bring it in postcss.config.js
module.exports = {
plugins: [
require('./test-plugin'),
require('autoprefixer')
]
};
PostCSS plugin Hello world
Write a plugin that inverts CSS property values
var postcss = require("postcss");
module.exports = postcss.plugin("postcss-backwards", function (opts) {
opts = opts || {};
return function (root, result) {
// Iterate over all style nodes
root.walkDecls((declaration) => {
declaration.value = declaration.value.split("").reverse().join("");
});
};
});
Of course this plugin, doesn’t make any sense, we just use it to learn how to write PostCSS plugin
postcss-multiple-themes
Usage
JS entry file import 2 style files
import "./default-theme.less";
import "./dark-theme.less";
component.less
.box{
width: 100px;
height: 100px;
border: 1px solid @border;
background-color: @bg;
color: @color;
}
default-theme.less
@import "./component";
@border: #333;
@color: #000;
@bg: #fff;
dark-theme.less
@import "./component";
@border: #999;
@color: #fff;
@bg: #000;
Output css
.box {
width: 100px;
height: 100px;
border: 1px solid #333;
background-color: #fff;
color: #000;
}
.dark .box {
border: 1px solid #999;
background-color: #000;
color: #fff;
}
Source Code
function isEmpty(arr) {
return Array.isArray(arr) && arr.length === 0;
}
const hasColorProp = (colorProps, declProp) =>
colorProps.some((prop) => declProp.includes(prop));
module.exports = (opts = {}) => {
if (!opts.colorProps) {
opts.colorProps = ["color", "background", "border", "box-shadow", "stroke"];
}
return (root) => {
let theme;
const file = root.source.input.file || "";
const matched = file.match(
/(?<theme>[a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
);
if (matched && matched.groups.theme !== "default") {
theme = matched.groups.theme;
} else {
if (process.env.NODE_ENV == "test") {
theme = "test";
}
}
if (theme) {
root.walkRules((rule) => {
rule.walkDecls((decl) => {
if (!hasColorProp(opts.colorProps, decl.prop)) {
decl.remove();
}
});
if (isEmpty(rule.nodes)) {
rule.remove();
} else {
rule.selector = rule.selector
.replace(/\n/g, "")
.split(",")
.map((s) => `.${theme} ${s}`)
.join(",\n");
}
});
}
};
};
Implementation steps
1、Use the file name to determine if a skin style needs to be generated
const file = root.source.input.file || "";
const matched = file.match(
/(?<theme>[a-zA-Z0-9]+)-theme.(less|css|scss|sass)/
);
2、Remove styles that do not contain colors, and leave border-color background-color
and the CSS properties that contains colors
["color", "background","border","box-shadow","stroke",]
3、If there are no CSS properties in the CSS selector, delete the selector
4、In front of the CSS selector .theme
class name
Upgrade of old project
The original project may not have color-sensitive variables in a separate style file, and the absolute value of color may be written in the style.
Is it possible to write a tool to help us upgrade?
At this time, I have a library that helps me,postcss-less will help us parse the less to AST ,Then we can configure the rules to replace the ** color with the variable **
configure the rules
module.exports = [
{
prop: ["background-color", "background"],
from: ["#fff", "#ffffff", "@white"],
to: "@component-background",
},
{
prop: ["border", "border-color"],
from: ["#D3D9E4", "#D3D9E2"],
to: "@border-color",
},
{
prop: ["color"],
from: ["#666E79", "#5C6268"],
to: "@text-color",
}
];
Transform
const syntax = require("postcss-less");
var fs = require("fs");
const path = require("path");
const rules = require("./rule.js");
var glob = require("glob");
function log(file, node, to) {
console.log(
"\x1b[32m",
`convert ${file} ${node.source.start.line}:${node.source.start.column} ${node.parent.selector} ${node.prop} from ${node.value} to ${to}`
);
}
let codes = {};
// options is optional
glob("./src/**/*.less", function (er, files) {
files.forEach((file) => {
var ast = syntax.parse(file);
// traverse AST and modify it
ast.walkDecls(function (node) {
rules.forEach((item) => {
if (item.prop.includes(node.prop) && item.from.includes(node.value)) {
node.value = item.to;
log(file, node, item.to);
}
});
});
fs.writeFileSync(path.resolve(file), syntax.nodeToString(ast));
});
});
Main steps
1、Read all the less files with glob
2、Use postcss-less
Convert less to AST
3、Iterating over all CSS properties, the decision is replaced with the less variable in the rule
4、Convert to less write file
The above code is the simplest, and there are many styles that are not covered
For example: border
You can write border-color
and so on.
Use VSCODE regular query missing color
When the above rules cannot cover all the project code, the developer can enter the rules in VSCODE.
(#[a-fA-F0-9]{3})|(#[a-fA-F0-9]{6})|^rgb
Find out the colors in the code and extract them into less variables one by one.
Summary
This article summarizes some common ways of front-end multiple skins. Through the most comparison, I find that it is the most convenient to generate skin style through PostCSS in our project, and it is also the easiest to make your website support dark mode. I opensource postcss-multiple-themes to Github and released the npm package.
Thinking about how to replace the css color in the old project with variables through PostCSS, when there are more projects, the labor cost is saved to a certain extent.
Last
If you are also doing change skin work for WebApp and are plagued by the problem of multi-skin in the front-end, I hope this article will be helpful to you. You can also exchange your ideas and experiences in the comments area. Welcome to explore the front-end.
Top comments (4)
This is IMHO best solution to the problem. But I have problems with my builds. Sometimes the builded css is perfectly fine, but sometime is broken, meaning that css is missing classes. I think it's related to my setup only, where I'm using it on ant design project and not all components have separate index.js and dark-theme.less and default-theme.lss. Only the root component has. Anyway I wanted to say thank you for inspiration.
Antd dynamic packaging will be some troubles. You can use Antd v4, It have css variables.
I want to preserve IE11 compatibility for some time. Anyway I found problem with the plugin. Will post PR.
Thank you very much.