Topic 11.5
Preprocessors and Tooling
In one line
Build tools transform the CSS you write into the CSS browsers download: Sass adds variables, mixins and nesting before the browser sees it, PostCSS runs plugins such as Autoprefixer, Tailwind generates utility classes from your markup, and minifiers make the final file as small as possible.
Think of it like this
A restaurant kitchen. The chef cooks in a big, messy kitchen with handwritten notes and shortcuts (your source files). What reaches the customer is a clean plate (the CSS file the browser downloads). CSS tooling is everything that happens in the kitchen between the two: translating shortcuts, adding what's missing for some customers, and trimming the plate so it arrives fast.
Words you'll meet
New words in this topic, in plain English. Come back here whenever one feels fuzzy.
- Build step
- A program that turns your source files into the files the browser downloads, before the site goes live.
- Preprocessor
- A language, like Sass, that compiles into plain CSS.
- SCSS
- Sass's main syntax: CSS plus variables, nesting, mixins and more, in
.scssfiles. - Mixin
- A named, reusable block of declarations in Sass, inserted with
@include, optionally with parameters. - PostCSS
- A tool that reads CSS into a tree, runs plugins to change it, and writes CSS back out.
- Vendor prefix
- A browser-specific beginning on a property name, like
-webkit-, used for features before they were standard. - Autoprefixer
- A PostCSS plugin that adds the vendor prefixes your target browsers still need.
- Browserslist
- A shared list of which browsers your project supports, read by Autoprefixer and other tools.
- Minification
- Rewriting a file into fewer bytes without changing what it does.
- Source map
- A file that tells DevTools which source line each part of the output came from.
Step by step
01Write SCSS
A Sass file uses variables, a mixin and nesting with string building. None of this reaches the browser as written.
$brand: #4f46e5;
$radius: 12px;
@mixin focus-ring($color) {
outline: 2px solid $color;
outline-offset: 2px;
}
.card {
border-radius: $radius;
&__title { color: $brand; }
&:focus-visible { @include focus-ring($brand); }
}02Compile it to CSS
Dart Sass (npx sass card.scss card.css) replaces variables with values, pastes the mixin, and expands nesting into flat selectors.
.card {
border-radius: 12px;
}
.card__title {
color: #4f46e5;
}
.card:focus-visible {
outline: 2px solid #4f46e5;
outline-offset: 2px;
}03Run PostCSS plugins
PostCSS reads a config file listing its plugins, in order. Autoprefixer reads your Browserslist targets from package.json or a .browserslistrc file.
module.exports = {
plugins: [
require("autoprefixer"),
require("cssnano")({ preset: "default" }),
],
};04See what Autoprefixer adds
For targets that need it, a standard property gets a prefixed copy before it. When every target supports the standard name, Autoprefixer adds nothing, and it also removes outdated prefixes you wrote by hand.
/* you wrote */
.title { user-select: none; }
/* Autoprefixer output for targets that include older Safari */
.title { -webkit-user-select: none; user-select: none; }05Minify and ship
The minifier removes everything the browser doesn't need. The server then compresses the file, and a hashed file name lets browsers cache it for a year.
.card{border-radius:12px}.card__title{color:#4f46e5}.card:focus-visible{outline:2px solid #4f46e5;outline-offset:2px}06Or let Tailwind generate the CSS
With Tailwind you write classes in the markup, and its build step scans your files and generates only the utilities you used. Class names must appear complete in the source: building them from pieces ("bg-" + color) hides them from the scanner.
@import "tailwindcss";
@theme {
--color-brand: #4f46e5;
}
/* now classes like bg-brand and text-brand exist */Try it yourself
- 1
Change one token
In the first live example, change
--radiusto4px. Predict the new padding the console prints (it is computed from the radius). Run and check. - 2
Minify by hand
In the second live example, add
border: 0px solid #000000;to.a, then write the shortest equivalent in.b. Add"border"to the property list and check it printssame.
Code & diagrams
@use "sass:math";
$sizes: (sm: 0.875rem, md: 1rem, lg: 1.25rem);
// a loop that writes three classes
@each $name, $size in $sizes {
.text-#{$name} { font-size: $size; }
}
// a function
@function rem($px) { @return math.div($px, 16) * 1rem; }
.card { padding: rem(24); } // -> padding: 1.5rem;Variables, maths, nesting and a darker hover colour (`color-mix()`) all work natively. The `&--danger` line is the Sass habit that native CSS drops, which is why the flat `.btn--danger` rule below it is the one doing the work.
Preview
Console
Nothing logged yet.Expected console output
default: rgb(79, 70, 229)
danger: rgb(220, 38, 38)
padding: 6px 12px`#ffffff` became `#fff`, `0px` became `0`, four margin values became three (the left copies the right), and the comment and whitespace are gone. The computed styles are identical.
Preview
Console
Nothing logged yet.Expected console output
color same
background-color same
padding same
margin same<button class="rounded-lg bg-indigo-600 px-4 py-2 font-semibold text-white
hover:bg-indigo-700 focus-visible:outline-2 focus-visible:outline-offset-2
md:px-6 dark:bg-indigo-400 dark:text-gray-900">
Save
</button>
<!-- md: = inside @media (width >= 48rem); hover: = on :hover; dark: = dark mode -->Break it on purpose
Errors are the best teachers. Make each change, read the error, guess what went wrong, then reveal the answer.
Break #1
Build Tailwind class names from pieces
In a Tailwind project, write <div class={"bg-" + color + "-500"}> with color = "red".
Break #2
Expect Sass variables to change at run time
In Sass, write $brand: blue; inside .dark { ... } and expect .button { color: $brand; } to turn blue inside .dark.
Myth vs fact
Myth
Every project needs Sass.
Fact
Native CSS now has variables, nesting, maths, colour mixing and layers. Sass still helps with loops, functions and mixins, but many modern projects use plain CSS plus a small PostCSS or Lightning CSS step.
Myth
Tailwind CSS files are huge.
Fact
Tailwind generates only the classes it finds in your source files, so typical production CSS is a few tens of kilobytes before compression.
Myth
You still need to write -webkit- prefixes by hand.
Fact
Most prefixes are no longer needed, and the remaining ones are best added by Autoprefixer from your browser targets, which also removes outdated ones.
Pro corner
Extra depth for experienced readers. New to this? Skip it for now and come back later.
- ▸
Lightning CSS (written in Rust) parses, transforms, prefixes and minifies CSS in one fast pass, and can lower modern syntax like nesting for older targets. Vite can use it instead of PostCSS, and Tailwind v4 uses it internally.
- ▸
Minifiers must be careful with things that look redundant but aren't: two declarations of the same property are often a deliberate fallback (
width: 100%; width: stretch;), so good minifiers keep both when the second might be unsupported. - ▸
CSS is render-blocking, so after minification the next win is splitting: inline the small critical CSS needed for the first screen in the
<head>and load the rest normally. Measure with Lighthouse's "Eliminate render-blocking resources" before doing it; it adds build complexity. - ▸
Browserslist queries like
defaults(> 0.5%, last 2 versions, Firefox ESR, not dead) are shared by Autoprefixer, Babel and others, so one list keeps CSS and JavaScript targets in sync.npx browserslistprints the browsers your query covers.
Remember this
- 1
A build step is a program that runs on your computer or server before the site is published and turns source files into output files. The browser only ever sees the output. That's why tools can add features browsers don't have: they translate them away. Popular bundlers like Vite, webpack, Parcel and the Next.js compiler run the CSS tools for you.
- 2
Sass is a preprocessor: a language that compiles to CSS. Its common syntax is SCSS (
.scssfiles, which look like CSS with extras). It gives you$variables, nesting with&(including&__titlestring building), mixins (reusable blocks of declarations with parameters, used with@include), functions and maths, loops, and partials split across files and loaded with@use. The official compiler is Dart Sass; the older Node Sass and LibSass are deprecated, and Sass's own@importis deprecated in favour of@use. - 3
Native CSS has caught up with much of Sass: custom properties (variables that also change at run time, which Sass variables can't), nesting (Topic 11.1),
calc(),clamp(),color-mix()and cascade layers. Sass variables are replaced by values at build time and don't exist in the browser; custom properties live in the browser, inherit and can change per element or theme. Many new projects need no preprocessor. - 4
PostCSS is not a language; it's a tool that parses CSS into a tree and runs plugins over it, then prints CSS again. Each plugin does one job. Autoprefixer adds vendor prefixes (browser-specific names like
-webkit-that some features needed before they were standard) based on your Browserslist target list (e.g.> 0.5%, last 2 versions, not dead) and data from Can I Use. postcss-preset-env lets you write future CSS and translates it for older browsers. cssnano minifies. Tailwind itself can run as a PostCSS plugin. - 5
Tailwind CSS is a utility-first framework (Topic 11.4). You write classes like
flex gap-4 p-6 md:grid-cols-2 hover:bg-indigo-700 dark:text-whitein your HTML. Tailwind scans your source files for class names and generates CSS for only the classes it finds, so the output stays small. Prefixes likemd:,hover:anddark:are variants that wrap a utility in a media query or state. Since version 4 (2025), configuration lives in CSS (@import "tailwindcss";and an@theme { --color-brand: ...; }block), and the theme values become real custom properties. - 6
Minification rewrites CSS into the fewest bytes with the same meaning: it removes comments and whitespace, drops the last semicolon in a block, shortens colours (
#ffffffto#fff), zeros (0pxto0), and can merge duplicate rules. Tools: cssnano, Lightning CSS, esbuild. After that, the server compresses the file with gzip or Brotli for the trip over the network, which usually shrinks CSS by 70–90%. Together with long-term caching and file names that contain a content hash (main.3f9a1c.css), this is what makes CSS fast to deliver (see/system-designon caching and CDNs). - 7
Source maps connect the output back to the source. When a minified file is created, the tool also writes a
.mapfile; DevTools reads it and shows youcard.scssline 12 instead of line 1, column 48,203 ofmain.css. Keep them in development, and decide on purpose whether to publish them in production.
Explain it without notes
What is a CSS preprocessor? Compare Sass variables with CSS custom properties.
What is PostCSS, and how is it different from Sass?
What does Autoprefixer do, and how does it decide what to add?
How does Tailwind keep its CSS small, and what is the main pitfall of that approach?
What happens to CSS between your editor and the browser in a typical production build?
Practice
Rewrite this Sass with native CSS only: $gap: 16px; .grid { gap: $gap; .item { padding: $gap / 2; } }.
Minify this by hand without changing its meaning: .box { margin: 0px 0px 0px 0px; color: #ff0000; /* red */ }.
Trade-offs
- ↔
A build step unlocks features and optimisation but adds configuration, dependencies and a gap between what you wrote and what DevTools shows (bridged by source maps).
- ↔
Sass adds power (loops, functions, mixins) that plain CSS lacks, but its variables are static and its syntax drifts from native CSS, for example
&__nesting.
Done when you can
I can explain what a build step does to CSS.
I know what Sass adds, and how its variables differ from custom properties.
I can explain PostCSS, Autoprefixer and Browserslist.
I can explain how Tailwind generates CSS and why class names must be complete.
I know what minification, compression, hashing and source maps each do.