An Introduction to PostCSS

Craig Buckler
Share

CSS preprocessors are popular, but they have some drawbacks. In this introduction to PostCSS, we’ll explore the advantages of PostCSS, how it works, and what its extensive range of plugins can achieve.

Processing CSS with PostCSS

The Value and Limitations of Preprocessors

Most CSS developers are familiar with preprocessors. Tools including Sass, Less, and Stylus introduced concepts such as file partials, nesting, variables, and mixins. Some features are gradually appearing in native CSS, but a preprocessor is still useful for managing large codebases and maintaining style and coding consistency.

It may be difficult to imagine life without a CSS preprocessor, but there are downsides:

  • Preprocessors are not extendable or limitable. Most preprocessors are a black box which provide you a specific set of supported features. It may be possible to write custom functions, but functionality beyond the scope of that tool is remains impossible — such as inlining an SVG as a background image.

    Similarly, you can’t stop developers using options you’d rather avoid such as @extend or deep nesting. Linting can help, but it won’t stop the preprocessor compiling a valid file.

  • Preprocessors provide their own syntax. Preprocessor code may resemble CSS, but no browser can parse the file natively. The syntax is different and, if your tool changes or is unavailable, your code will require updates to make it usable.

The benefits more than outweigh these risks, but there is an alternative …

What is PostCSS?

PostCSS is not a preprocessor (although it can behave like one). It’s a Node.js tool which takes valid CSS and enhances it. Even those using Sass, Less, or Stylus often run a PostCSS step after the initial CSS compilation. You may have encountered the PostCSS Autoprefixer plugin which automatically prepends -webkit, -moz, and -ms vendor prefixes to CSS properties which require them.

On its own, PostCSS does nothing. It’s a parser which tokenizes CSS code to create an abstract syntax tree. A plugin can process this tree and update properties accordingly. Once all plugins have completed their work, PostCSS reformats everything back into a string and outputs to a CSS file.

Around 350 plugins are available, and most perform a single task such as inlining @import declarations, simplifying calc() functions, handling image assets, syntax linting, minifying, and more. A more user friendly plugin search is available at the PostCSS plugins catalogue.

PostCSS benefits include:

  • You start with standard CSS. PostCSS is to CSS what Babel is to JavaScript. It can take a standard stylesheet which works in recent browsers and output CSS which works everywhere — for example, transpiling the newer inset property back into top, bottom, left, and right properties. Over time, you could drop this process as more browsers support inset.

    Admittedly, some plugins allow you to parse preprocessor-like syntax which isn’t standard CSS, but you don’t have to use them.

  • Use the plugins and features you need. PostCSS is configurable, and you can adopt the plugins you require. For example, you could support partials and nesting but not permit variables, loops, mixins, maps, and other features available in Sass.

  • Provide a custom configuration for every project. A individual project configuration can enhance or reduce the set of plugins used elsewhere. The options are far more varied than any preprocessor.

  • Write your own PostCSS plugins. A wide range of plugins is available for extending syntax, parsing future properties, adding fallbacks, optimizing code, processing colors, images, fonts, and even writing CSS in other languages such as Spanish and Russian.

    In the unlikely event you can’t find what you need, you can write your own PostCSS plugin in JavaScript.

  • You’re possibly using PostCSS already. You may be able to remove your preprocessor dependencies if you’re already running a PostCSS plugin such as AutoPrefixer. PostCSS is not necessarily faster or more lightweight than using a preprocessor, but it can handle all CSS processing in a single step.

Installing PostCSS

PostCSS requires Node.js, but this tutorial demonstrates how to install and run PostCSS from any folder — even those that aren’t Node.js projects. You can also use PostCSS from webpack, Parcel, Gulp.js, and other tools, but we’ll stick to the command line.

Install PostCSS globally on your system by running the following:

npm install -g postcss-cli

Ensure it’s working by entering this:

postcss --help

Installing Your First PostCSS Plugin

You’ll require at least one plugin to do anything practical. The PostCSS import plugin is a good option which inlines all @import declarations and merges your CSS into a single file. Install it globally like so:

npm install -g postcss-import

To test this plugin, open or create a new project folder such as cssproject, then create a src subfolder for your source files. Create a main.css file to load all partials:

/* src/main.css */
@import '_reset';
@import '_elements';

Then create a _reset.css file in the same folder:

/* src/reset.css */
* {
  padding: 0;
  margin: 0;
}

Follow this with an _elements.css file:

/* src/elements.css */
body {
  font-family: sans-serif;
}

label {
  user-select: none;
}

Run PostCSS from the project’s root folder by passing the input CSS file, a list of plugins to --use, and an --output filename:

postcss ./src/main.css --use postcss-import --output ./styles.css

If you don’t have any errors, the following code will be output to a new styles.css file in the project root:

/* src/main.css */
/* src/reset.css */
* {
  padding: 0;
  margin: 0;
}
/* src/elements.css */
body {
  font-family: sans-serif;
}
label {
  user-select: none;
}
/* sourceMappingURL=data:application/json;base64,...

Note that PostCSS can output CSS files anywhere, but the output folder must exist; it will not create the folder structure for you.

Enabling and Disabling Source Maps

An inline source map is output by default. When the compiled CSS file is used in an HTML page, examining it in the browser’s developer tools will show the original src file and line. For example, viewing <body> styles will highlight src/_elements.css line 2 rather than styles.css line 8.

You can create an external source map by adding a --map (or -m) switch to the postcss command. There’s little benefit other than the CSS file is cleaner and the browser doesn’t need to load the source map unless the developer tools are open.

You can remove the source map with --no-map. Always use this option when outputting CSS files for production deployment.

Install and Use the AutoPrefixer Plugin

The Autoprefixer plugin is often a developer’s first encounter with PostCSS. It adds vendor prefixes according to browser usage and rules defined at caniuse.com. Vendor prefixes are less used in modern browsers which hide experimental functionality behind flags. However, there are still properties such as user-select which require -webkit-, -moz-, and -ms- prefixes.

Install the plugin globally with this:

npm install -g autoprefixer

Then reference it as another --use option on your postcss command:

postcss ./src/main.css --use postcss-import --use autoprefixer --output ./styles.css

Examine the label declaration from line 11 of styles.css to view the vendor-prefixed properties:

label {
  -webkit-user-select: none;
     -moz-user-select: none;
      -ms-user-select: none;
          user-select: none;
}

AutoPrefixer uses the browserlist module to determine which browser prefixes to add. The default is:

  • > 0.5%: a browser with at least 0.5% market share
  • last 2 versions: the last two releases of those browsers
  • Firefox ESR: including Firefox Extended Support Releases
  • not dead: any browser that is not discontinued

You can change these defaults by creating a .browserslistrc file. For example:

> 2%

Or you can add a "browserslist" array to package.json in a Node.js project. For example:

"browserslist": [
   "> 2%"
]

Targeting browsers with a 2% or more market share only requires a -webkit- prefix in Safari:

label {
  -webkit-user-select: none;
          user-select: none;
}

Minify CSS with cssnano

cssnano minifies CSS by stripping whitespace, comments, and other unnecessary characters. Results will vary, but you can expect a 30% file reduction which you can deploy to production servers for better web page performance.

Install cssnano globally:

npm install -g cssnano

Then add it to your postcss command. We’ll also include --no-map to disable the source map:

postcss ./src/main.css --use postcss-import --use autoprefixer --use cssnano --no-map --output ./styles.css

This reduces the CSS file to 97 characters:

*{margin:0;padding:0}body{font-family:sans-serif}label{-webkit-user-select:none;user-select:none}

Automatically Build when Source Files Change

The PostCSS --watch option automatically builds your CSS file when any of the source files change. You may also want to add the --verbose switch which reports when a build occurs:

postcss ./src/main.css --use postcss-import --use autoprefixer --use cssnano --no-map --output ./styles.css --watch --verbose

Your terminal will show Waiting for file changes. Make a change to any file and styles.css is rebuilt. PostCSS will also report any problems such as syntax errors.

To finish, press Ctrl | Cmd + C in the terminal.

Create a PostCSS Configuration File

The postcss command will become long and cumbersome as you add further plugins and options. You can create a JavaScript configuration file which defines all the options and can logically determine whether it’s running in a development or production environment.

Create a configuration file named postcss.config.cjs in the root of your project folder. Aso note the following:

  • you can put the file in another folder, but you’ll need to specify --config <dir> when running postcss
  • you can use postcss.config.js as the file name, but PostCSS may fail in Node.js projects which have "type": "module" set in package.json

Add the following code to postcss.config.cjs:

// PostCSS configruation
module.exports = (cfg) => {

  const devMode = (cfg.env === 'development');

  return {

    map: devMode ? 'inline' : null,
    plugins: [
      require('postcss-import')(),
      require('autoprefixer')(),
      devMode ? null : require('cssnano')()
    ]

  };

};

PostCSS passes a cfg object which contains the command line options. For example:

{
  cwd: '/home/yourname/cssproject',
  env: undefined,
  options: {
    map: { inline: true },
    parser: undefined,
    syntax: undefined,
    stringifier: undefined
  },
  file: {
    dirname: '/home/yourname/cssproject/src',
    basename: 'main.css',
    extname: '.css'
  }
}

The module must return an object with optional properties:

  • map: the source map setting
  • parser: whether to use a non-CSS syntax parser (such as the scss plugin)
  • plugins: an array of plugins and configurations to process in the order specified

The code above detects whether the postcss command has an --env option. This is a shortcut for setting the NODE_ENV environment variable. To compile CSS in development mode, run postcss with --env development and, optionally, set --watch --verbose. This creates an inline source map and does not minify the output:

postcss ./src/main.css --output ./styles.css --env development --watch --verbose

To run in production mode and compile minified CSS without a source map, use this:

postcss ./src/main.css --output ./styles.css

Ideally, you could run these as terminal or npm scripts to reducing typing effort further.

PostCSS Progress

You now know the basics of PostCSS. Enhancing the functionality is a matter of adding and configuring further plugins. Invest some time and you’ll soon have a workflow you can adapt for any web project.

The tutorial on how to use PostCSS as a configurable alternative to Sass provides more configuration examples and plugin options.

Further links: