Using the PostCSS plugin Let your WebApp support dark mode
Tags: javascript, css
Published: 2021-09-18 05:50:58

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/i5t9eangmc23nzt7xx5p.png)
 
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.

```css
: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

```css
: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.


```html
<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

```js
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

```js
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) .
 
![image astexplorer](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/9z3pipnpvvoz81ohgxzt.png)
 

### Purpose

![less file image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/jnh8erpmcw3wgkx48t32.png)

Currently I have one less style and two color variables, I need to generate the following style:



![output image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/lzy94fpwqf73xcsdkc53.png)



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`.

```js
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](https://github.com/postcss/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。

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/apmbzzqfn2ep51fvhp8q.png)
 

Of course we can just create a JS file in the project root directory

```js
// 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` 

```js
module.exports = {
  plugins: [
    require('./test-plugin'),
    require('autoprefixer')
  ]
};
```

### PostCSS plugin Hello world

Write a plugin that inverts CSS property values

```js
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**

```js
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**

```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

```js
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

```js
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](https://github.com/shellscape/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

```js
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

```js
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.

![image](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/uav5ni46nco7toelo6hj.png) 


## Summary


1. 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](https://github.com/maqi1520/postcss-multiple-themes) to Github and released the [npm package](https://www.npmjs.com/package/postcss-multiple-themes).

2. 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.