Module 10: Tooling & Testing
Linting, Formatting and Build Tools: Stylelint, PostCSS, Autoprefixer and Bundlers
What each piece of the CSS toolchain does, how to configure it, and what actually happens when your stylesheet is processed for production.
Plain CSS works with no tooling at all. As a project grows, a small toolchain catches mistakes, enforces consistency, and prepares your CSS for the browsers you support. Each tool has one job, and you should know what that job is so you can tell what is happening when something breaks.
source CSS ─► lint ─► format ─► PostCSS (prefix, modernize) ─► minify ─► bundled .css
catch keep support older browsers smaller what ships
errors style and new syntax files
Linting with Stylelint#
A linter reads your CSS without running it and reports problems: invalid properties, duplicate selectors, overly specific rules, missing fallbacks.
npm install --save-dev stylelint stylelint-config-standard
{
"extends": ["stylelint-config-standard"],
"rules": {
"selector-max-id": 0,
"selector-max-specificity": "0,3,0",
"declaration-no-important": true,
"max-nesting-depth": 3,
"color-named": "never"
}
}
Save that as .stylelintrc.json, and run it:
npx stylelint "src/**/*.css"
npx stylelint "src/**/*.css" --fix
The extended config provides sensible defaults. The rules after it are choices that enforce the lessons from this course: no ID selectors, a cap on specificity, no !important, and limited nesting depth. A rule like selector-max-specificity makes your cascade problems visible in code review, instead of in the browser six months later.
Stylelint also has an editor extension. Seeing a squiggle under colour: red as you type beats finding it in DevTools later.
Formatting with Prettier#
A formatter decides layout: indentation, spacing, line breaks, quotes. Its value is ending style arguments.
npm install --save-dev prettier
npx prettier --write "src/**/*.css"
Keep the roles separate. Prettier formats, Stylelint checks quality. If your Stylelint config includes stylistic rules that conflict with Prettier, use a config that turns them off. Running both in a pre-commit hook (for example with lint-staged) makes sure that nothing unformatted reaches the repository.
PostCSS: transforming CSS with plugins#
PostCSS parses your CSS into a tree, passes it through plugins, and writes it back out. It does nothing itself. The plugins are the tool.
// postcss.config.js
module.exports = {
plugins: [
require("autoprefixer"),
require("cssnano")({ preset: "default" }),
],
};
Autoprefixer and Browserslist#
Some newer properties need vendor prefixes (-webkit-, -moz-) in certain browsers. Autoprefixer adds only the prefixes that your target browsers need. You describe those targets in a browserslist field:
{
"browserslist": ["> 0.5%", "last 2 versions", "not dead"]
}
/* you write */
.box { user-select: none; }
/* autoprefixer outputs, if a target browser needs it */
.box { -webkit-user-select: none; user-select: none; }
Browserslist data comes from usage statistics, so the output changes as browsers evolve. Hand-writing prefixes goes out of date silently, and one forgotten prefix is a bug that only shows in one browser. Let the tool do it, and keep the same Browserslist for all your tools (Babel and others read it too).
Other useful plugins#
postcss-preset-envconverts modern CSS syntax into something older browsers understand, based on your targets.postcss-importinlines@importstatements at build time, solving the performance problem you saw in Module 1.cssnanoor Lightning CSS minify: they remove whitespace and comments, shorten colors and merge rules. A cleaned file is often 20 to 40 percent smaller.
Bundlers: Vite, webpack, Parcel#
A bundler follows imports across files, applies transforms, and produces optimized output. For CSS it can:
- Let you
import "./styles.css"from JavaScript, and extract it to a separate cached file for production. - Compile Sass or Less automatically.
- Generate content-hashed file names (
styles.4f9a1c.css) so you can cache them forever and still ship updates instantly. - Inline small assets such as icons and split CSS per page.
With Vite, a project needs very little configuration. PostCSS runs automatically if a config file is present:
// main.js
import "./styles/main.css";
{
"scripts": {
"dev": "vite",
"build": "vite build",
"lint:css": "stylelint \"src/**/*.css\"",
"format": "prettier --write \"src/**/*.{css,js,html}\""
}
}
CSS Modules and scoped styles#
A CSS Module is a file named something.module.css. The build tool rewrites its class names to be unique, and gives you a map:
/* card.module.css */
.title { font-size: 1.25rem; }
import styles from "./card.module.css";
element.className = styles.title; // becomes something like "card_title__x7Gk2"
Names can never collide across files, which removes the global scope problem at its root. This is the standard approach in many React projects. The cost is that styling from outside a component needs deliberate hooks like custom properties.
Removing unused CSS#
Over time a stylesheet accumulates rules nobody uses. The Coverage tab in DevTools estimates what is unused on a page. Automated removal tools exist (PurgeCSS being the best known), but they work by searching your source for class names. They break if you build class names dynamically, like "btn-" + color, since the tool cannot find those strings. Utility frameworks such as Tailwind avoid the issue by generating only what they find in the first place.
What a production pipeline should do#
- Lint and format on every commit.
- Bundle and minify.
- Add vendor prefixes for the browsers you actually target.
- Output hashed file names and serve with long cache headers.
- Serve compressed (gzip or Brotli). CSS compresses very well.
- Inline critical CSS only when measurements show the benefit.
Common mistakes#
- Writing vendor prefixes by hand.
- No shared Browserslist, so tools disagree about what to support.
- Overly strict lint rules that the team turns off. Start small and tighten gradually.
- Running two formatters that fight each other.
- Purging dynamic class names, and shipping a page with missing styles.
- Never looking at the output. Open the built CSS once in a while to check it.
- Using a build tool for a tiny site that did not need one.
- Committing without running the linter.
Practice#
- Set up Stylelint with the config above on a small project. Introduce an ID selector and an
!importantand see the errors. - Add Prettier and format a messy file. Check what changed.
- Add Autoprefixer and set Browserslist to
last 2 versions, then todefaults, and compare the output foruser-selectandbackdrop-filter. - Build the same CSS with and without minification and compare sizes, then compare the compressed (gzip) size.