Tailwind CSS: Design Tokens & style.css — Quick Guide
URL: https://ruvebal.github.io/web-atelier-udit/lessons/en/tailwind/design-tokens/
📋 Table of Contents
- How CSS Post-Processing Works
- Example: Tailwind v3 Setup
- Example: Tailwind v4 Setup (CSS-First)
- Rationale Behind CSS Post-Processing
- Best Practices
Design Tokens in Tailwind and How They Relate to style.css
Example:
- In Tailwind v3:
- You have a
style.cssfile with@tailwind base;,@tailwind components;, and@tailwind utilities;.- Your design tokens (such as colors and spacing) are defined in
tailwind.config.js.- When you build, PostCSS processes
style.css, and Tailwind reads your config, generating utility classes for those tokens.
- In Tailwind v4:
- Your
style.cssimports Tailwind with@import "tailwindcss";.- Your design tokens are written as CSS custom properties inside
@theme { ... }blocks directly in your CSS.- PostCSS processes
style.cssand Tailwind picks up the tokens from the@themeblock to generate utilities.
How CSS Post-Processing Works
Tailwind CSS is a PostCSS plugin that transforms your CSS during build time. Here’s the actual flow:
style.css(ortailwind.css) is your entry point — the source file that gets processed.- PostCSS reads this file and runs plugins in sequence (Tailwind → Autoprefixer → cssnano).
- Tailwind plugin reads
tailwind.config.js(v3) or@themedirectives (v4) to understand what tokens to generate. - Tailwind scans your content files (HTML, JS, etc.) to detect which utility classes are actually used.
- Tailwind generates only the CSS utilities you need, based on your config and actual usage.
- Output is a processed CSS file with all utilities, base styles, and your custom CSS combined.
The Relationship Between Files
-
style.css: The source file that PostCSS processes. Contains:@tailwinddirectives (base,components,utilities) that tell Tailwind where to inject generated CSS- Your custom CSS (global styles, component classes, overrides)
- In v4:
@themeblocks that define design tokens directly in CSS
-
tailwind.config.js(v3): The configuration file that defines:- Design tokens (colors, spacing, typography, etc.)
- Content paths (where to scan for class usage)
- Plugin configuration
- Theme extensions
-
postcss.config.js: Defines the processing pipeline (which plugins run and in what order)
Tailwind v3 vs v4: Configuration Approaches
Tailwind CSS v3 (Recommended: CSS Variables + Config Mapping):
- Recommended: Tokens defined as CSS variables in
@layer base { :root { ... } }instyle.css, then referenced intailwind.config.jsusingvar(--token-name) - Alternative: Tokens defined directly in
tailwind.config.js(JavaScript object) - CSS file imports Tailwind via
@tailwinddirectives - PostCSS processes CSS → Tailwind reads JS config → generates utilities
- Key syntax:
@layer base { :root { --token: value; } }in CSS +var(--token)in config - Benefits: Single source of truth, runtime flexibility, better tooling support
Tailwind CSS v4 (CSS-first):
- Tokens defined directly in CSS using
@theme { ... }blocks (no:rootwrapper needed) - No
tailwind.config.jsrequired (though still supported for compatibility) - CSS file uses
@import "tailwindcss"and@theme { ... }blocks - Key syntax:
@theme { --token: value; }in CSS (replaces@layer base { :root { ... } }) - More aligned with native CSS, leverages CSS custom properties
- Zero-config content detection (automatically scans project files)
The Difference:
- v3: Use
@layer base { :root { --token: value; } }in CSS, then map to utilities intailwind.config.js - v4: Use
@theme { --token: value; }directly in CSS (no@layer baseor:rootneeded)
TL;DR
style.cssis the entry point that PostCSS processes — it’s where Tailwind gets invoked.- v3 (Recommended): Define tokens as CSS variables in
style.css, reference them intailwind.config.js. - v4: Define tokens directly in CSS using
@themeblocks. - PostCSS transforms your CSS source file using Tailwind as a plugin.
- Tailwind generates utilities based on your config and actual class usage in your content files.
- Principle: CSS holds the values, Tailwind maps them to utilities.
Example: Tailwind v3 Setup
Recommended Approach: CSS Variables + Config Mapping
The recommended pattern for Tailwind v3 is to define tokens as CSS variables in style.css and reference them in tailwind.config.js. This provides a single source of truth, runtime flexibility, and better tooling support.
1. Define Tokens in CSS (style.css)
Define your design token values as CSS custom properties:
/* style.css */
@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
/* ===== RESPONSIVE TYPOGRAPHY TOKENS ===== */
/* Font sizes: min, preferred (vw-based), max */
--text-2xs: clamp(0.25rem, 0.3vw + 0.15em, 0.5rem);
--text-xs: clamp(0.375rem, 0.4vw + 0.2em, 0.625rem);
--text-sm: clamp(0.5rem, 0.5vw + 0.25em, 0.75rem);
--text-base: clamp(0.625rem, 0.6vw + 0.3em, 0.875rem);
--text-lg: clamp(0.75rem, 0.8vw + 0.4em, 1rem);
--text-xl: clamp(0.875rem, 1vw + 0.5em, 1.2rem);
--text-2xl: clamp(1rem, 1.5vw + 0.8em, 1.5rem);
--text-3xl: clamp(1.125rem, 2vw + 1em, 2rem);
--text-4xl: clamp(1.25rem, 2.5vw + 1.2em, 2.5rem);
--text-5xl: clamp(1.5rem, 3vw + 1.5em, 3.25rem);
--text-6xl: clamp(1.75rem, 3.5vw + 1.8em, 4rem);
--text-7xl: clamp(2rem, 4vw + 2em, 4.5rem);
/* ===== LINE HEIGHT TOKENS ===== */
--line-height-2xs: 1.2;
--line-height-xs: 1.2;
--line-height-sm: 1.35;
--line-height-base: 1.7;
--line-height-lg: 1.6;
--line-height-xl: 1.35;
--line-height-2xl: 1.25;
--line-height-3xl: 1.2;
--line-height-4xl: 1.15;
--line-height-5xl: 1.1;
--line-height-6xl: 1.05;
--line-height-7xl: 1.05;
/* ===== LETTER SPACING TOKENS ===== */
--letter-spacing-xl: -0.005em;
--letter-spacing-2xl: -0.01em;
--letter-spacing-3xl: -0.01em;
--letter-spacing-4xl: -0.015em;
--letter-spacing-5xl: -0.02em;
--letter-spacing-6xl: -0.025em;
--letter-spacing-7xl: -0.03em;
/* ===== COLOR TOKENS ===== */
--color-primary-50: #eff6ff;
--color-primary-500: #3b82f6;
--color-primary-900: #1e3a8a;
--color-surface-light: #f8fafc;
--color-surface-dark: #1e293b;
--color-content: #0f172a;
--color-content-muted: #64748b;
--color-content-inverted: #ffffff;
}
}
/* Your custom component styles */
@layer components {
.btn-primary {
@apply bg-primary-500 text-white px-4 py-2 rounded;
}
}
2. Reference Tokens in Config (tailwind.config.js)
Map CSS variables to Tailwind utilities. You can either override colors (replace Tailwind’s defaults) or extend them (add new colors while keeping defaults):
Option A: Override Colors (Replace Tailwind Defaults)
When you define theme.colors (not in extend), you replace Tailwind’s default color palette. Use this pattern to preserve defaults while overriding specific semantic colors:
// tailwind.config.js
import { colors as defaultColors } from 'tailwindcss/defaultTheme';
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
// Reference CSS variables for font sizes
fontSize: {
'2xs': ['var(--text-2xs)', { lineHeight: 'var(--line-height-2xs)' }],
xs: ['var(--text-xs)', { lineHeight: 'var(--line-height-xs)' }],
sm: ['var(--text-sm)', { lineHeight: 'var(--line-height-sm)' }],
base: ['var(--text-base)', { lineHeight: 'var(--line-height-base)' }],
lg: ['var(--text-lg)', { lineHeight: 'var(--line-height-lg)' }],
xl: ['var(--text-xl)', { lineHeight: 'var(--line-height-xl)', letterSpacing: 'var(--letter-spacing-xl)' }],
'2xl': ['var(--text-2xl)', { lineHeight: 'var(--line-height-2xl)', letterSpacing: 'var(--letter-spacing-2xl)' }],
'3xl': ['var(--text-3xl)', { lineHeight: 'var(--line-height-3xl)', letterSpacing: 'var(--letter-spacing-3xl)' }],
'4xl': ['var(--text-4xl)', { lineHeight: 'var(--line-height-4xl)', letterSpacing: 'var(--letter-spacing-4xl)' }],
'5xl': ['var(--text-5xl)', { lineHeight: 'var(--line-height-5xl)', letterSpacing: 'var(--letter-spacing-5xl)' }],
'6xl': ['var(--text-6xl)', { lineHeight: 'var(--line-height-6xl)', letterSpacing: 'var(--letter-spacing-6xl)' }],
'7xl': ['var(--text-7xl)', { lineHeight: 'var(--line-height-7xl)', letterSpacing: 'var(--letter-spacing-7xl)' }],
},
// Override colors: preserve defaults, add custom semantic colors
colors: {
// Preserve Tailwind's default colors (red, blue, green, etc.)
...defaultColors,
// Override/add semantic colors using CSS variables
// These are commonly used by component libraries (e.g., shadcn/ui)
border: 'var(--color-border)',
input: 'var(--color-input)',
ring: 'var(--color-ring)',
background: 'var(--color-background)',
foreground: 'var(--color-foreground)',
// Semantic colors with foreground variants
// Use: bg-primary, text-primary-foreground
primary: {
DEFAULT: 'var(--color-primary)',
foreground: 'var(--color-primary-foreground)',
},
secondary: {
DEFAULT: 'var(--color-secondary)',
foreground: 'var(--color-secondary-foreground)',
},
destructive: {
DEFAULT: 'var(--color-destructive)',
foreground: 'var(--color-destructive-foreground)',
},
muted: {
DEFAULT: 'var(--color-muted)',
foreground: 'var(--color-muted-foreground)',
},
accent: {
DEFAULT: 'var(--color-accent)',
foreground: 'var(--color-accent-foreground)',
},
popover: {
DEFAULT: 'var(--color-popover)',
foreground: 'var(--color-popover-foreground)',
},
card: {
DEFAULT: 'var(--color-card)',
foreground: 'var(--color-card-foreground)',
},
},
extend: {
// Extend: Add new colors without replacing defaults
// Use: bg-surface-light, text-content-muted
colors: {
surface: {
light: 'var(--color-surface-light)',
dark: 'var(--color-surface-dark)',
},
content: {
DEFAULT: 'var(--color-content)',
muted: 'var(--color-content-muted)',
inverted: 'var(--color-content-inverted)',
},
},
// Other tokens can also reference CSS variables
spacing: {
18: '4.5rem',
88: '22rem',
},
borderRadius: {
'4xl': '2rem',
},
},
},
plugins: [],
};
Understanding defaultColors and Color Override
import { colors as defaultColors } from 'tailwindcss/defaultTheme':
This syntax:
import { colors as defaultColors } from 'tailwindcss/defaultTheme'
does the following:
- Imports the default Tailwind CSS color palette (like blue, red, green, gray, etc.) from Tailwind’s internal theme.
- The variable
defaultColorswill hold an object that includes all standard Tailwind color names, each with their full range of shades (like 50, 100, 200, up to 900), such asdefaultColors.red[500]. - When you use the spread operator
...defaultColorsinside yourtheme.colorsconfiguration, it copies all these built-in colors into your config. This means you keep access to Tailwind’s default color utility classes (such asbg-blue-500,text-red-700, etc.) rather than replacing them entirely.
In summary:
import { colors as defaultColors } ...gets all default color objects...defaultColorsadds them to your own palette so that default utility classes remain available alongside your custom colors
Color Override vs Extend:
theme.colors(override): Replaces the entire color palette. Use...defaultColorsto preserve defaults while adding overrides.theme.extend.colors(extend): Adds new colors without affecting existing ones. Best for adding brand colors alongside defaults.
Semantic Color Pattern (with foreground variants):
primary: {
DEFAULT: 'var(--color-primary)', // bg-primary, border-primary
foreground: 'var(--color-primary-foreground)', // text-primary-foreground
},
This pattern is used by component libraries (like shadcn/ui) to ensure proper contrast:
bg-primary+text-primary-foreground= guaranteed readable text on primary background- Useful for buttons, cards, and other components where text color must match background
Option B: Extend Colors (Keep All Defaults)
If you only want to add new colors without overriding, use extend:
// tailwind.config.js
/** @type {import('tailwindcss').Config} */
export default {
content: ['./index.html', './src/**/*.{js,ts,jsx,tsx}'],
theme: {
fontSize: {
// ... font sizes ...
},
extend: {
// Extend: Adds new colors, keeps all Tailwind defaults
colors: {
primary: {
50: 'var(--color-primary-50)',
500: 'var(--color-primary-500)',
900: 'var(--color-primary-900)',
},
surface: {
light: 'var(--color-surface-light)',
dark: 'var(--color-surface-dark)',
},
// All default Tailwind colors (red, blue, etc.) remain available
},
},
},
};
Benefits of This Approach
- Single source of truth: Values live in CSS, not duplicated in JavaScript
- Easier maintenance: Change values in one place (
style.css) - Runtime flexibility: CSS variables can be changed dynamically via JavaScript or media queries
- Clear separation: CSS = tokens (values), Config = mapping (utilities)
- Tooling-friendly: CSS variables work seamlessly with browser DevTools
- Responsive tokens: Use
clamp()and viewport units directly in CSS variables
Why This Works
- CSS defines the design tokens (the actual values)
- Tailwind config maps tokens to utilities (the class names)
- PostCSS processes both and generates the final CSS
- Principle: CSS holds the values, Tailwind maps them to utilities
This pattern follows the principle: CSS holds the values, Tailwind maps them to utilities.
3. PostCSS Configuration: postcss.config.js
Defines the processing pipeline:
// postcss.config.js
module.exports = {
plugins: [
require('tailwindcss'), // Tailwind processes @tailwind directives
require('autoprefixer'), // Adds vendor prefixes
...(process.env.NODE_ENV === 'production'
? [require('cssnano')({ preset: 'default' })] // Minifies in production
: []),
],
};
Example: Tailwind v4 Setup (CSS-First)
In Tailwind v4, you define tokens directly in your CSS file using @theme blocks (no @layer base or :root wrapper needed):
/* style.css */
@import 'tailwindcss';
/* Define design tokens directly in CSS using @theme */
/* Note: No @layer base { :root { ... } } wrapper needed in v4 */
@theme {
/* Colors */
--color-primary-50: #eff6ff;
--color-primary-500: #3b82f6;
--color-primary-900: #1e3a8a;
--color-surface-light: #f8fafc;
--color-surface-dark: #1e293b;
--color-content: #0f172a;
--color-content-muted: #64748b;
--color-content-inverted: #ffffff;
/* Spacing */
--spacing-18: 4.5rem;
--spacing-88: 22rem;
/* Typography */
--font-family-display: ui-sans-serif, system-ui, 'Segoe UI', Inter, Roboto, 'Helvetica Neue', Arial, 'Noto Sans',
sans-serif;
--font-family-body: ui-sans-serif, system-ui, Inter, Roboto, 'Helvetica Neue', Arial, 'Noto Sans', sans-serif;
/* Border radius */
--radius-4xl: 2rem;
/* Box shadow */
--shadow-elevated: 0 12px 30px -12px rgba(59, 130, 246, 0.35);
}
/* Your custom CSS */
@layer components {
.btn-primary {
@apply bg-primary-500 text-white px-4 py-2 rounded;
}
}
Side-by-Side Comparison: v3 vs v4 Syntax
Tailwind v3 (Recommended approach):
@layer base {
:root {
--text-base: clamp(0.625rem, 0.6vw + 0.3em, 0.875rem);
--color-primary-500: #3b82f6;
}
}
Then reference in tailwind.config.js:
fontSize: {
base: ['var(--text-base)', { lineHeight: '1.7' }],
},
colors: {
primary: {
500: 'var(--color-primary-500)',
},
},
Tailwind v4:
@theme {
--text-base: clamp(0.625rem, 0.6vw + 0.3em, 0.875rem);
--color-primary-500: #3b82f6;
}
No config file needed — Tailwind automatically generates utilities from @theme tokens.
Key differences in v4:
- Syntax:
@theme { ... }replaces@layer base { :root { ... } } - No
tailwind.config.jsrequired (though still supported for compatibility) - Tokens defined as CSS custom properties in
@themeblocks - More aligned with native CSS standards
- Zero-config content detection (automatically scans project files)
Rationale Behind CSS Post-Processing
Why PostCSS?
PostCSS is a tool that transforms CSS using JavaScript plugins. It’s not a preprocessor (like Sass) or a postprocessor in the traditional sense — it’s a CSS transformer that runs during your build process.
Benefits of the PostCSS approach:
-
Tree-shaking: Tailwind scans your content files and generates only the utilities you actually use. This dramatically reduces the final CSS file size (from ~3MB of possible utilities to only what you need, often <50KB).
-
Just-in-Time (JIT) compilation: In Tailwind v3+, utilities are generated on-demand based on your markup. If you write
bg-primary-500, Tailwind generates that specific class. If you never usebg-purple-900, it’s never generated. -
Plugin ecosystem: PostCSS enables a rich plugin ecosystem. Your build pipeline can include:
- Tailwind: Generates utility classes
- Autoprefixer: Adds vendor prefixes (
-webkit-,-moz-, etc.) - cssnano: Minifies CSS for production
- PostCSS plugins: Custom transformations
-
Source of truth: Your
style.cssfile is the entry point. PostCSS processes it, and the output is a single, optimized CSS file that browsers can consume.
The Build Process Flow
┌─────────────────┐
│ style.css │ ← Source file (you write this)
│ (@tailwind...) │
└────────┬────────┘
│
▼
┌─────────────────┐
│ PostCSS │ ← Reads CSS, runs plugins
└────────┬────────┘
│
├──► Tailwind Plugin
│ ├── Reads tailwind.config.js (v3) or @theme (v4)
│ ├── Scans content files for class usage
│ └── Generates utility classes
│
├──► Autoprefixer
│ └── Adds vendor prefixes
│
└──► cssnano (production only)
└── Minifies CSS
│
▼
┌─────────────────┐
│ output.css │ ← Final processed CSS (browser consumes this)
│ (utilities + │
│ your CSS) │
└─────────────────┘
The Role of Build Tools (Vite, Webpack, Rollup, etc.)
Build tools like Vite, Webpack, or Rollup orchestrate the entire build process, including CSS processing. Here’s how they integrate PostCSS and Tailwind:
How Build Tools Integrate PostCSS
Vite (and most modern build tools) automatically detects and processes PostCSS:
// vite.config.js (or vite.config.ts)
import { defineConfig } from 'vite';
export default defineConfig({
// Vite automatically detects postcss.config.js
// No additional configuration needed for PostCSS!
// Optional: Customize CSS processing
css: {
postcss: './postcss.config.js', // Explicit path (optional)
// or inline PostCSS config:
// postcss: {
// plugins: [
// require('tailwindcss'),
// require('autoprefixer'),
// ],
// },
},
});
How it works:
-
CSS Import Detection: When Vite encounters
import './style.css'in your JavaScript/TypeScript files, it recognizes it as a CSS dependency. -
Automatic PostCSS Processing: Vite automatically:
- Reads
postcss.config.js(orpostcss.config.cjs) - Processes the CSS file through PostCSS plugins
- Runs Tailwind, Autoprefixer, and other plugins in sequence
- Reads
-
Development Mode (
vite dev):- Processes CSS on-demand as files change
- Hot Module Replacement (HMR) updates styles without full page reload
- Fast refresh for instant feedback
-
Production Mode (
vite build):- Processes all CSS files
- Runs cssnano for minification (if configured)
- Bundles and optimizes CSS output
- Generates source maps (if enabled)
Complete Build Flow with Vite
┌──────────────────┐
│ main.js │ ← Entry point
│ import './style.css' │ ← CSS imported via JS
│ OR │
│ index.html │ ← Entry point
│ <link rel="stylesheet" href="./style.css"> │ ← CSS linked directly in HTML
└────────┬─────────┘
│
▼
┌──────────────────┐
│ Vite │ ← Build tool orchestrates everything
└────────┬─────────┘
│
├──► JavaScript Bundling (esbuild/rollup)
│
└──► CSS Processing
│
▼
┌──────────────────┐
│ style.css │ ← Vite extracts CSS import
│ (@tailwind...) │
└────────┬─────────┘
│
▼
┌──────────────────┐
│ PostCSS │ ← Vite invokes PostCSS
└────────┬─────────┘
│
├──► Tailwind Plugin
│ ├── Reads tailwind.config.js
│ ├── Scans content files
│ └── Generates utilities
│
├──► Autoprefixer
│
└──► cssnano (production)
│
▼
┌──────────────────┐
│ output.css │ ← Final CSS
│ (or injected │
│ into <style>) │
└──────────────────┘
Standalone PostCSS vs Build Tool Integration
Standalone PostCSS (CLI - used in some setups):
# Direct PostCSS CLI usage
postcss src/style.css -o dist/style.css
# Or with watch mode
postcss src/style.css -o dist/style.css --watch
- Useful for static site generators (Jekyll, Hugo)
- Explicit control over CSS processing
- Run separately from JavaScript bundling
Build Tool Integration (Vite, Webpack, Rollup):
<!-- In your index.html -->
<link rel="stylesheet" href="/style.css" />
Or
// In your JavaScript/TypeScript
import './style.css'; // Vite processes this automatically
- Integrated into the build pipeline
- Automatic processing when CSS is imported
- HMR support in development
- CSS code-splitting in production
Why Build Tools Matter
Build tools like Vite provide:
- Unified Pipeline: JavaScript, CSS, and other assets processed together
- Hot Module Replacement: CSS changes reflect instantly in development
- Optimization: Automatic code-splitting, tree-shaking, and minification
- Type Safety: TypeScript support for CSS imports (with proper plugins)
- Performance: Fast development builds with esbuild, optimized production builds
Example with Vite:
// src/main.js
import { createApp } from 'vue';
import './style.css'; // ← Vite processes this through PostCSS/Tailwind
createApp(App).mount('#app');
When you run vite dev, Vite:
- Processes
style.cssthrough PostCSS (Tailwind → Autoprefixer) - Injects processed CSS into the page
- Watches for changes and updates via HMR
When you run vite build, Vite:
- Processes all CSS imports
- Generates optimized, minified CSS bundle
- Extracts CSS to separate file (or inlines small CSS)
Why Not Just Write Regular CSS?
You could write regular CSS, but Tailwind’s approach offers:
- Consistency: Design tokens ensure consistent spacing, colors, and typography across your entire application.
- Maintainability: Change a token once, update everywhere it’s used.
- Developer experience: Utility classes are faster to write than custom CSS for common patterns.
- Performance: Only the CSS you use is included in the final bundle.
Best Practices
Token Definition
- v3 (Recommended): Define tokens as CSS variables in
style.css(in@layer base { :root { ... } }), then reference them intailwind.config.jsusingvar(--token-name). This provides a single source of truth and runtime flexibility. - v3 (Alternative): Define tokens directly in
tailwind.config.jsundertheme.extend. Simpler but less flexible. - v4: Define tokens in
@themeblocks in your CSS file. This aligns with native CSS and makes tokens more discoverable.
Using Tokens in Markup
- Avoid arbitrary values: Instead of
bg-[#3b82f6], usebg-primary-500(defined in your tokens). - Semantic naming: Use names like
content-mutedrather thangray-500to decouple color from meaning. - Consistent spacing: Use spacing tokens (
p-4,gap-8) rather than arbitrary values (p-[1.5rem]).
Custom CSS in style.css
Use style.css for:
- Global styles:
:rootvariables, resets, base element styling - Component classes: Reusable component styles using
@layer components - Complex selectors: Styles that require CSS features Tailwind doesn’t cover
- Third-party overrides: Adjusting styles from external libraries
Build Process
- Development: Run PostCSS in watch mode to see changes immediately
- Production: Ensure cssnano is enabled to minify the final output
- Content paths: In v3, ensure
contentintailwind.config.jsincludes all files where you use Tailwind classes