On this page
Advanced Customization in Tailwind v4
Advanced Customization in Tailwind v4
Tailwind v4 introduces a fundamentally different customization model compared to v3. All configuration moves into your CSS file using first-class directives: @theme, @source, @variant, and @utility. This lesson explores each in depth.
@theme deep dive
@theme registers CSS custom properties as design tokens. Every token you define generates a corresponding set of utility classes.
Naming conventions
The token name determines which utilities are generated:
| Token pattern | Generated utilities |
|---|---|
--color-NAME |
bg-NAME, text-NAME, border-NAME, ring-NAME, shadow-NAME, fill-NAME, stroke-NAME |
--color-NAME-NUMBER |
bg-NAME-NUMBER, text-NAME-NUMBER, etc. |
--font-NAME |
font-NAME |
--text-NAME |
text-NAME (with --text-NAME--line-height and --text-NAME--letter-spacing sub-properties) |
--spacing-NUMBER |
p-NUMBER, m-NUMBER, w-NUMBER, h-NUMBER, gap-NUMBER, etc. |
--breakpoint-NAME |
NAME: responsive variant prefix |
--shadow-NAME |
shadow-NAME |
--radius-NAME |
rounded-NAME |
--animate-NAME |
animate-NAME |
--ease-NAME |
ease-NAME |
Overriding default tokens
You can override any default Tailwind token in @theme:
@theme {
/* Override the default blue-500 */
--color-blue-500: oklch(55% 0.22 250);
/* Override the default border radius */
--radius-lg: 0.625rem;
/* Override the default sans font */
--font-sans: "Inter", ui-sans-serif, sans-serif;
/* Override the breakpoints entirely */
--breakpoint-sm: 576px;
--breakpoint-md: 768px;
--breakpoint-lg: 992px;
--breakpoint-xl: 1200px;
--breakpoint-2xl: 1400px;
}Resetting the default palette
To start with a completely empty palette (no default colors at all):
@theme {
/* Reset ALL color tokens */
--color-*: initial;
/* Now define only your own colors */
--color-primary: #6366f1;
--color-secondary: #f59e0b;
--color-neutral-50: #f9fafb;
--color-neutral-900: #111827;
}The initial keyword clears all tokens matching the --color-* wildcard.
@source
@source tells Tailwind which files to scan for class names. By default, Tailwind auto-detects files in your project. Be explicit when you need to include or exclude specific paths.
@import "tailwindcss";
/* Scan your application source files */
@source "../src/**/*.{html,ts,tsx,jsx,vue,svelte}";
/* Scan a component library in node_modules */
@source "../node_modules/my-ui-lib/dist/**/*.js";
/* Use a glob pattern to exclude test files */
@source "../src/**/!(*.test|*.spec).{ts,tsx}";Safelisting dynamic classes
If you generate class names dynamically (from a database, API, or user input), Tailwind cannot detect them by scanning. Use @source inline() to safelist specific values:
/* Safelist complete class strings */
@source inline("bg-red-500 bg-green-500 bg-blue-500 bg-yellow-500");
/* Safelist a range using curly-brace expansion */
@source inline("bg-{red,green,blue,yellow,purple}-{500,600,700}");
/* Safelist all indigo shades */
@source inline("{bg,text,border}-indigo-{50,100,200,300,400,500,600,700,800,900}");@variant
@variant creates custom pseudo-class variant prefixes that you can use anywhere in your HTML.
Basic @variant syntax
/* Simple pseudo-class variant */
@variant hocus {
&:hover,
&:focus-visible {
@slot;
}
}
/* Data attribute variants */
@variant loading (&[data-loading="true"]);
@variant selected (&[data-selected="true"]);
@variant expanded (&[aria-expanded="true"]);
@variant checked-item (&[aria-checked="true"]);
/* Ancestor-based variants */
@variant sidebar-collapsed (&:where(.sidebar-collapsed *));
@variant theme-minimal (&:where([data-theme="minimal"] *));After defining these, use them as prefixes in HTML:
<!-- Loading state -->
<button data-loading="true" class="loading:opacity-70 loading:cursor-wait">Save</button>
<!-- Expanded state on an accordion -->
<button aria-expanded="false" class="expanded:text-indigo-700 expanded:bg-indigo-50">
Toggle section
</button>
<!-- Combined hover + focus -->
<a href="/page" class="hocus:text-indigo-600 hocus:underline transition-colors">
Navigation link
</a>@utility
@utility defines custom single-purpose utility classes that integrate fully into Tailwind — they work with every variant and appear in IntelliSense.
/* Scrollbar utilities */
@utility scrollbar-hide {
scrollbar-width: none;
&::-webkit-scrollbar { display: none; }
}
@utility scrollbar-thin {
scrollbar-width: thin;
}
/* Text effects */
@utility text-shadow {
text-shadow: 0 2px 4px rgb(0 0 0 / 0.12);
}
@utility text-shadow-sm {
text-shadow: 0 1px 2px rgb(0 0 0 / 0.10);
}
/* Mask utilities */
@utility mask-fade-b {
mask-image: linear-gradient(to bottom, black 60%, transparent 100%);
}
@utility mask-fade-r {
mask-image: linear-gradient(to right, black 60%, transparent 100%);
}
/* Layout utilities */
@utility center-abs {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
}
/* Glass effect */
@utility glass {
background: rgb(255 255 255 / 0.1);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border: 1px solid rgb(255 255 255 / 0.2);
}Usage with variants:
<!-- Works with every variant -->
<div class="scrollbar-hide hover:scrollbar-thin">Scrollable area</div>
<h1 class="text-shadow dark:text-shadow-sm">Heading with shadow</h1>
<div class="mask-fade-b sm:mask-fade-r">Fades on small, fades right on large</div>
<div class="glass dark:bg-white/5">Glassmorphism card</div>Performance optimization
CSS output analysis
The Tailwind CLI and Vite plugin both produce optimized, purged CSS output automatically. To analyze what is included:
# Build and check output size
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --minify
ls -lh dist/output.cssA typical production build with 200+ utilities used is around 8–15 KB gzipped.
Lightning CSS features
Tailwind v4's Oxide engine uses Lightning CSS for processing, which automatically:
- Adds vendor prefixes — no need for Autoprefixer
- Supports CSS nesting — native
&selector nesting works in all browsers - Handles custom properties — with proper fallbacks for older browsers
- Minifies the output — removes whitespace, deduplicates rules, shortens values
Content scanning performance
To make Tailwind's scanner faster, be specific with @source:
/* Too broad (scans everything including node_modules): */
/* @source "../**/*.html" */
/* Better (specific to your source): */
@source "../src/**/*.{html,ts}";
@source "../public/**/*.html";Integrating Tailwind plugins
In v4, CSS-based plugins use @plugin:
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@plugin "@tailwindcss/forms";
@plugin "@tailwindcss/container-queries";For JavaScript-based plugins that use the v3 plugin API, they still work via @plugin:
@plugin "./plugins/my-custom-plugin.js";But the preferred v4 approach is to write your customizations directly in CSS using @theme, @variant, and @utility instead of JavaScript plugins.
Practice
- Build a complete design token file in
@themefor a SaaS product: brand colors (50–950 scale in OKLCH), semantic surface colors, typography scale, and custom shadows. - Define three
@variantrules:loading,selected, andexpanded. Use them in an accordion component. - Create three custom
@utilityclasses:scrollbar-hide,glass, andtext-balance-pretty(applying bothtext-wrap: balanceandtext-wrap: pretty). Use them with hover and dark variants.
@import "tailwindcss";
/* ============================================
@theme: complete design token system
============================================ */
@theme {
/* === Colors === */
/* Brand palette (generates bg-brand-*, text-brand-*, etc.) */
--color-brand-50: oklch(97% 0.015 265);
--color-brand-500: oklch(57% 0.240 265);
--color-brand-600: oklch(50% 0.240 265);
--color-brand-700: oklch(42% 0.210 265);
/* Semantic surface tokens */
--color-surface: oklch(100% 0 0);
--color-surface-raised: oklch(98% 0 0);
--color-on-surface: oklch(10% 0 0);
--color-on-surface-muted:oklch(45% 0 0);
/* === Typography === */
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
--font-display: "Cal Sans", "Inter", sans-serif;
--font-mono: "Fira Code", ui-monospace, monospace;
/* Custom text sizes with line-height and letter-spacing */
--text-display: 4.5rem;
--text-display--line-height: 1;
--text-display--letter-spacing: -0.025em;
/* === Spacing === */
--spacing-18: 4.5rem;
--spacing-22: 5.5rem;
/* === Breakpoints === */
--breakpoint-xs: 480px;
--breakpoint-3xl: 1920px;
/* === Shadows === */
--shadow-glow: 0 0 24px oklch(57% 0.24 265 / 0.35);
--shadow-card: 0 1px 3px oklch(0% 0 0 / 0.08), 0 4px 12px oklch(0% 0 0 / 0.06);
/* === Border radius === */
--radius-4xl: 2rem;
--radius-5xl: 2.5rem;
/* === Animations === */
--animate-fade-in: fade-in 0.3s ease-out both;
--animate-slide-up: slide-up 0.4s cubic-bezier(0.16,1,0.3,1) both;
--animate-scale-in: scale-in 0.2s ease-out both;
}
/* === Custom keyframes === */
@keyframes fade-in {
from { opacity: 0; }
to { opacity: 1; }
}
@keyframes slide-up {
from { opacity: 0; transform: translateY(20px); }
to { opacity: 1; transform: translateY(0); }
}
@keyframes scale-in {
from { opacity: 0; transform: scale(0.95); }
to { opacity: 1; transform: scale(1); }
}
Sign in to track your progress