|$ curl https://forge-ai.dev/api/markdown?path=docs/css/preprocessors
$cat docs/css-preprocessors.md
updated Recently·35 min read·published

CSS Preprocessors

CSSToolsBeginner to Advanced
Introduction

CSS preprocessors extend the capabilities of vanilla CSS with features like variables, nesting, mixins, functions, and modular file organization. They compile to standard CSS for the browser. The two dominant options are Sass/SCSS and PostCSS, with utility-first frameworks like Tailwind CSS offering a fundamentally different approach.

Choosing between Sass, PostCSS, and Tailwind depends on your team's workflow, design system complexity, and performance requirements. Each has strengths that make it suitable for different kinds of projects.

Sass & SCSS

Sass (Syntactically Awesome Style Sheets) is the most mature CSS preprocessor. It has two syntaxes: the indented syntax (.sass) and SCSS (.scss), which is a superset of CSS. SCSS is the more popular choice as it allows gradual adoption in existing CSS projects.

info

Use the .scss extension for new projects. It is a superset of CSS, meaning any valid CSS file is also valid SCSS. This makes migration from vanilla CSS seamless.
styles.scss
SCSS
1// Variables — single source of truth for design tokens
2$primary: #00FF41;
3$secondary: #6C63FF;
4$background: #0A0A0A;
5$surface: #14141E;
6$text: #E0E0E0;
7$text-muted: #A0A0A0;
8$border: #222222;
9$radius: 8px;
10$font-mono: "JetBrains Mono", "Fira Code", monospace;
11$spacing-unit: 8px;
12
13// Nesting — mirrors HTML structure
14.card {
15 background: $surface;
16 border: 1px solid $border;
17 border-radius: $radius;
18 padding: $spacing-unit * 3;
19
20 .card-header {
21 display: flex;
22 align-items: center;
23 gap: $spacing-unit * 2;
24 margin-bottom: $spacing-unit * 2;
25
26 h3 {
27 color: $text;
28 font-size: 1.125rem;
29 margin: 0;
30 }
31
32 .badge {
33 background: $primary;
34 color: $background;
35 padding: 2px 8px;
36 border-radius: 4px;
37 font-size: 0.75rem;
38 }
39 }
40
41 .card-body {
42 color: $text-muted;
43 line-height: 1.6;
44 font-size: 0.875rem;
45 }
46
47 // & references the parent selector
48 &:hover {
49 border-color: $primary;
50 box-shadow: 0 0 20px rgba($primary, 0.1);
51 }
52}
53
54// Partials and @use (Sass modules)
55// File: _variables.scss
56// File: _mixins.scss
57// File: _typography.scss
58// File: _components.scss
59
60// Main file — imports partials
61@use "variables" as *;
62@use "mixins" as *;
63@use "typography";
64@use "components";
65
66// @use is the modern replacement for @import (deprecated)
67// It provides namespacing and only compiles once
Mixins, Functions & Control Flow

Mixins and functions enable reusable logic in stylesheets. Mixins output CSS rules, while functions return values. Sass also supports control flow like loops and conditionals.

mixins-functions.scss
SCSS
1// === MIXINS ===
2
3// Responsive breakpoints
4@mixin respond-to($breakpoint) {
5 @if $breakpoint == "sm" {
6 @media (min-width: 640px) { @content; }
7 } @else if $breakpoint == "md" {
8 @media (min-width: 768px) { @content; }
9 } @else if $breakpoint == "lg" {
10 @media (min-width: 1024px) { @content; }
11 } @else if $breakpoint == "xl" {
12 @media (min-width: 1280px) { @content; }
13 }
14}
15
16// Flexbox shortcut
17@mixin flex($direction: row, $align: center, $justify: center, $gap: 0) {
18 display: flex;
19 flex-direction: $direction;
20 align-items: $align;
21 justify-content: $justify;
22 gap: $gap;
23}
24
25// Text truncation
26@mixin truncate($lines: 1) {
27 @if $lines == 1 {
28 white-space: nowrap;
29 overflow: hidden;
30 text-overflow: ellipsis;
31 } @else {
32 display: -webkit-box;
33 -webkit-line-clamp: $lines;
34 -webkit-box-orient: vertical;
35 overflow: hidden;
36 }
37}
38
39// Usage
40.sidebar {
41 @include flex(column, stretch, flex-start, 8px);
42 padding: 16px;
43
44 .nav-item {
45 @include truncate(1);
46 }
47}
48
49// === FUNCTIONS ===
50
51@function spacing($multiplier: 1) {
52 @return $spacing-unit * $multiplier;
53}
54
55@function to-rem($px) {
56 @return ($px / 16px) * 1rem;
57}
58
59@function contrast-color($bg) {
60 @if lightness($bg) > 60% {
61 @return #0A0A0A;
62 } @else {
63 @return #E0E0E0;
64 }
65}
66
67// === LOOPS ===
68$sizes: ("sm": 4px, "md": 8px, "lg": 16px, "xl": 24px);
69
70@each $name, $size in $sizes {
71 .gap-#{$name} {
72 gap: $size;
73 }
74}
75
76// Generates:
77// .gap-sm { gap: 4px; }
78// .gap-md { gap: 8px; }
79// .gap-lg { gap: 16px; }
80// .gap-xl { gap: 24px; }

best practice

Prefer the @use module system over @import (deprecated). @use creates namespaced modules, prevents duplicate compilation, and makes dependency tracking explicit. Use @forward to re-export modules through a barrel file.
PostCSS

PostCSS is not a preprocessor — it is a tool for transforming CSS with JavaScript plugins. Think of it as Babel for CSS. It processes CSS through a pipeline of plugins, each performing a specific transformation. This modularity makes it extremely flexible.

postcss.config.js
JavaScript
1// postcss.config.js — PostCSS configuration
2module.exports = {
3 plugins: [
4 // Autoprefixer — adds vendor prefixes automatically
5 require("autoprefixer")({
6 flexbox: "no-2009",
7 grid: "autoplace",
8 }),
9
10 // postcss-preset-env — enables future CSS syntax
11 require("postcss-preset-env")({
12 stage: 2, // Stage 2+ CSS features
13 features: {
14 "nesting-rules": true, // Native CSS nesting
15 "custom-properties": {
16 preserve: true, // Keep CSS vars as fallback
17 },
18 "custom-media-queries": true,
19 "gap-properties": true,
20 },
21 autoprefixer: false, // Already using Autoprefixer
22 }),
23
24 // postcss-import — resolve @import statements
25 require("postcss-import")(),
26
27 // postcss-nested — unwrap nested rules (Sass-like)
28 require("postcss-nested")(),
29
30 // Custom media queries
31 require("postcss-custom-media")({
32 extensions: {
33 "--sm": "(min-width: 640px)",
34 "--md": "(min-width: 768px)",
35 "--lg": "(min-width: 1024px)",
36 "--xl": "(min-width: 1280px)",
37 },
38 }),
39
40 // postcss-mixins — Sass-like mixins
41 require("postcss-mixins")({
42 mixins: {
43 truncate: {
44 "white-space": "nowrap",
45 overflow: "hidden",
46 "text-overflow": "ellipsis",
47 },
48 },
49 }),
50 ],
51};
postcss-input-output.css
CSS
1/* Input — modern CSS syntax processed by PostCSS */
2
3/* Nesting (postcss-nesting or postcss-nested) */
4.card {
5 background: #14141e;
6 border: 1px solid #222;
7
8 & .header {
9 font-size: 1.25rem;
10 }
11
12 &:hover {
13 border-color: #00ff41;
14 }
15}
16
17/* Custom media queries */
18@custom-media --dark (prefers-color-scheme: dark);
19@custom-media --motion-ok (prefers-reduced-motion: no-preference);
20
21@media (--dark) {
22 body { background: #0a0a0a; }
23}
24
25@media (--motion-ok) {
26 .card { transition: transform 0.3s; }
27}
28
29/* Custom selectors */
30@custom-selector :--heading h1, h2, h3, h4, h5, h6;
31
32:--heading {
33 font-family: "Inter", sans-serif;
34 line-height: 1.2;
35}
36
37/* Output — processed by PostCSS */
38.card {
39 background: #14141e;
40 border: 1px solid #222;
41}
42.card .header {
43 font-size: 1.25rem;
44}
45.card:hover {
46 border-color: #00ff41;
47}
PluginPurposePostCSS Version
autoprefixerAdd vendor prefixes based on browser targetsEssential
postcss-preset-envFuture CSS syntax today (staged features)Essential
postcss-importInline @import statementsEssential
postcss-nestedSass-like nesting syntaxCommon
cssnanoCSS minification (production)Production
postcss-custom-mediaCustom media query aliasesOptional
postcss-mixinsSass-like mixin supportOptional
stylelintCSS linting (via postcss plugin)Dev workflow
🔥

pro tip

PostCSS with postcss-preset-env on stage: 2is the closest you can get to "write tomorrow's CSS today." It enables CSS nesting, custom media queries, and other CSSWG-approved features without the overhead of a full Sass compilation step.
Configuring with Vite & Webpack

Both Sass and PostCSS integrate cleanly with modern bundlers. Here is how to set up each combination.

build-config.ts
TypeScript
1// Vite — built-in support for PostCSS and Sass
2
3// Install Sass: npm install --save-dev sass
4// Install PostCSS deps: npm install --save-dev postcss autoprefixer postcss-preset-env
5
6// Vite auto-detects:
7// 1. postcss.config.js at project root
8// 2. .scss/.sass files (if sass is installed)
9// 3. .less files (if less is installed)
10// 4. .styl files (if stylus is installed)
11
12// vite.config.ts — no extra config needed for basic setup
13import { defineConfig } from "vite";
14import react from "@vitejs/plugin-react";
15
16export default defineConfig({
17 css: {
18 modules: {
19 localsConvention: "camelCaseOnly",
20 generateScopedName: "[name]__[local]___[hash:base64:5]",
21 },
22 preprocessorOptions: {
23 scss: {
24 additionalData: `@use "@/styles/variables" as *;`,
25 api: "modern-compiler", // Use the new Sass API
26 },
27 },
28 devSourcemap: true,
29 },
30});
31
32// Webpack — manual loader configuration
33module.exports = {
34 module: {
35 rules: [
36 // SCSS with PostCSS
37 {
38 test: /\.scss$/,
39 use: [
40 "style-loader", // Injects CSS into DOM
41 "css-loader", // Resolves CSS imports
42 {
43 loader: "postcss-loader",
44 options: {
45 postcssOptions: {
46 plugins: [
47 "autoprefixer",
48 "postcss-preset-env",
49 ],
50 },
51 },
52 },
53 "sass-loader", // Compiles SCSS to CSS
54 ],
55 },
56 ],
57 },
58};

warning

Vite's api: "modern-compiler" for Sass requires sass package version 1.70+. The old API (default) uses dart-sass synchronously, which can block the dev server. The modern API uses the new compileString API and is faster.
Tailwind CSS

Tailwind CSS takes a fundamentally different approach: instead of writing custom CSS, you compose utility classes directly in your HTML. It generates a massive set of atomic classes from a configurable design system, then purges unused styles in production.

tailwind-config.ts
TypeScript
1// tailwind.config.ts — Tailwind v4 configuration
2import type { Config } from "tailwindcss";
3
4export default {
5 content: [
6 "./index.html",
7 "./src/**/*.{js,ts,jsx,tsx}",
8 ],
9 theme: {
10 extend: {
11 colors: {
12 primary: "#00FF41",
13 surface: "#14141E",
14 muted: "#A0A0A0",
15 border: "#222222",
16 },
17 fontFamily: {
18 mono: ["JetBrains Mono", "Fira Code", "monospace"],
19 },
20 spacing: {
21 "4.5": "1.125rem",
22 },
23 animation: {
24 "fade-in": "fadeIn 0.3s ease-in-out",
25 },
26 keyframes: {
27 fadeIn: {
28 "0%": { opacity: "0" },
29 "100%": { opacity: "1" },
30 },
31 },
32 },
33 },
34 plugins: [
35 require("@tailwindcss/typography"),
36 require("@tailwindcss/forms"),
37 require("@tailwindcss/aspect-ratio"),
38 ],
39} satisfies Config;
40
41// postcss.config.js (needed for Tailwind)
42module.exports = {
43 plugins: {
44 "@tailwindcss/postcss": {},
45 autoprefixer: {},
46 },
47};
48
49// Or use Tailwind v4 with Vite (simpler):
50// npm install @tailwindcss/vite
51// Add to vite.config.ts:
52import tailwindcss from "@tailwindcss/vite";
53export default defineConfig({
54 plugins: [tailwindcss()],
55});
TailwindComponent.tsx
TSX
1// Utility-first approach — components built from utilities
2function DashboardCard({ title, value, trend }: Props) {
3 return (
4 <div className="rounded-lg border border-[#222] bg-[#14141E] p-6
5 transition-colors hover:border-[#00FF41]/30">
6 <div className="flex items-center justify-between mb-4">
7 <h3 className="text-sm font-mono text-[#A0A0A0]">{title}</h3>
8 <span className={`text-xs font-mono ${
9 trend > 0 ? "text-[#00FF41]" : "text-[#EF4444]"
10 }`}>
11 {trend > 0 ? "+" : ""}{trend}%
12 </span>
13 </div>
14 <p className="text-2xl font-mono text-[#E0E0E0]">{value}</p>
15 </div>
16 );
17}
18
19// @apply directive — extract repeated utility patterns
20// Only use @apply for complex, repeated component patterns
21.card-base {
22 @apply rounded-lg border border-[#222] bg-[#14141E] p-6
23 transition-colors hover:border-[#00FF41]/30;
24}
25
26// Custom utilities (Tailwind v4)
27@utility text-gradient {
28 background: linear-gradient(to right, #00FF41, #6C63FF);
29 -webkit-background-clip: text;
30 -webkit-text-fill-color: transparent;
31 background-clip: text;
32}
33
34// Responsive design with utility classes
35<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
36 <div className="col-span-1 md:col-span-2 lg:col-span-1">
37 {/* Content */}
38 </div>
39</div>

info

Tailwind's @apply directive should be used sparingly. The primary benefit of Tailwind is that you can see styling directly in your markup. If you extract everything with @apply, you lose that visibility and end up back in a "name things" pattern similar to BEM.
Approach Comparison
DimensionSass/SCSSPostCSSTailwind CSS
ApproachPreprocessor (extends CSS)Postprocessor (transforms CSS)Utility-first framework
Learning CurveMedium (syntax differences)Low (native CSS features)Medium (utility class system)
File Size (prod)Depends on authored CSSSmallest (only what you write)Very small (purged unused)
Variables$variables (powerful)CSS custom propertiesTheme config (token-based)
NestingNative (SCSS)Via pluginN/A (utilities in markup)
Mixins / LoopsRich (SassScript)Via plugin (limited)N/A
ReusabilityMixins, extends, functionsCSS classes, pluginsUtility composition, components
Design SystemsManual (variables + partials)Manual (custom properties)Built-in (theme config)
Build SpeedSlow (dart-sass)Fast (JS plugins)Medium (scan + generate + purge)

best practice

There is no single best approach. Use Sass for complex design systems with heavy abstraction needs. Use PostCSS for projects that want to stay close to native CSS with progressive enhancement. Use Tailwind for rapid UI development with a strong design system foundation. Many teams combine them — Sass for design tokens and Tailwind for utility classes.
Recommended Workflows

Sass + PostCSS (Hybrid)

Many production projects use Sass for authoring (variables, mixins, nesting) and PostCSS for processing (autoprefixing, minification). This combines the power of Sass preprocessing with PostCSS's post-processing pipeline.

sass-postcss.js
JavaScript
1// postcss.config.js — Sass + PostCSS pipeline
2module.exports = {
3 plugins: [
4 "postcss-import", // Inline @imports before Sass handles them
5 "autoprefixer", // Add vendor prefixes
6 "postcss-preset-env", // Future CSS features
7 "cssnano", // Minify (production only)
8 ],
9};
10
11// package.json
12{
13 "scripts": {
14 "build:css": "sass src/styles:dist/styles && postcss dist/styles/*.css --replace"
15 }
16}
17
18// Or let Vite/Webpack handle the pipeline automatically

Tailwind + CSS Modules

Combining Tailwind with CSS Modules gives you utility classes for rapid prototyping and scoped component styles for complex layouts.

tailwind-modules.tsx
TSX
1// Component with Tailwind utilities + CSS Module
2import styles from "./Dashboard.module.css";
3
4export function Dashboard() {
5 return (
6 <div className={`grid grid-cols-3 gap-4 ${styles.dashboard}`}>
7 <div className="col-span-2">
8 <ChartWidget />
9 </div>
10 <div className={styles.sidebar}>
11 <div className="p-4 rounded-lg border border-[#222] bg-[#14141E]">
12 <ActivityFeed />
13 </div>
14 </div>
15 </div>
16 );
17}
18
19/* Dashboard.module.css */
20.dashboard {
21 padding: 24px;
22 max-width: 1440px;
23 margin: 0 auto;
24}
25
26.sidebar {
27 position: sticky;
28 top: 24px;
29 height: fit-content;
30 max-height: calc(100vh - 48px);
31 overflow-y: auto;
32}
Best Practices
Use @use instead of @import in Sass — @import is deprecated and causes duplication
Configure browserslist in package.json for consistent Autoprefixer and preset-env targets
Purge unused CSS in production — Tailwind does this automatically, for Sass use purgecss
Limit nesting depth to 3 levels maximum — deep nesting creates overly specific selectors
Use CSS custom properties for runtime-themeable values alongside preprocessor variables
Prefer utility classes for one-off styles and components for complex, reusable patterns
Use CSS Modules or Shadow DOM for scoped styles in component-based architectures
Keep your PostCSS config minimal — each plugin adds build time
Adopt postcss-preset-env stage 2 features before introducing a full preprocessor
Use Lightning CSS for production CSS minification — it is significantly faster than cssnano
$Blueprint — Engineering Documentation·Section ID: CSS-PP-01·Revision: 1.0