On-Demand CSS for Build Times Under 200 ms
Classic Tailwind CSS produced stylesheets of 3 MB and more during development. The JIT compiler solves this problem at its root: it scans template files in real time, generates only the classes that are actually needed on demand, and makes arbitrary values like [clamp(1rem,5vw,3rem)] work without any configuration.
Table of Contents
- 1. The Problem Before JIT: Massive Dev Bundles
- 2. How the Tailwind JIT Compiler Works
- 3. Arbitrary Values: Design Tokens Without Configuration
- 4. Content Configuration: What the JIT Compiler Scans
- 5. Vite Integration: HMR in Under 50 ms
- 6. PostCSS Setup for Classic Build Pipelines
- 7. Tailwind JIT in Magento Hyvä
- 8. JIT vs. Classic Build: A Direct Comparison
- 9. Common JIT Problems and Their Solutions
- 10. Summary
- 11. FAQ
1. The Problem Before JIT: Massive Dev Bundles
Before the introduction of the Tailwind JIT compiler, Tailwind CSS produced a complete stylesheet in the development environment, containing every combination of color classes, spacing utilities, responsive variants, and pseudo classes. The result was CSS files between 2 MB and 10 MB, depending on how many configuration variants were enabled. Browser DevTools became sluggish at that scale, CSS parsing measurably slowed page loads, and hot module replacement took several seconds because PostCSS had to regenerate the entire stylesheet from scratch.
The production build solved this problem with PurgeCSS or Tailwind v2's built-in content scan: any class that did not appear in the HTML, PHP, or JavaScript was removed from the final bundle. The dev build, however, remained untouched, and was therefore considerably slower than necessary. Developers worked with a different CSS bundle in development than in production, which caused subtle layout discrepancies that only surfaced at deploy time. It was exactly this drift between dev and prod that the Tailwind JIT concept set out to close.
2. How the Tailwind JIT Compiler Works
The Tailwind JIT compiler inverts the classic principle: instead of generating every possible class up front and purging afterward, it scans all configured template files at startup and generates only the classes that are actually used. When a new class appears in a file, that triggers the watcher, which adds the CSS for that single class within milliseconds. This means the CSS bundle, at any given moment, contains only what is genuinely needed right now.
Internally, the JIT compiler works with a regular-expression-based scanner that walks through all content files and extracts complete Tailwind class names, including modifiers like hover:, sm:, dark:, and stacked variants like group-hover:focus:. For every class found, the corresponding CSS block is generated on demand. That makes Tailwind JIT blazing fast even in projects with complex variant combinations: only the combinations that are actually used get generated, not the cartesian product of every possibility.
/* tailwind.config.js: JIT compiler configuration (Tailwind v3) */
/** @type {import('tailwindcss').Config} */
module.exports = {
/* content: defines which files the JIT scanner reads */
content: [
'./src/**/*.{html,js,php,phtml,vue,jsx,tsx}',
'./templates/**/*.phtml',
/* Safelist: always include these classes even if not found in templates */
/* Use sparingly, every safelisted class bypasses the JIT scanner */
],
safelist: [
/* Dynamic classes assembled at runtime (e.g. from CMS data) */
{ pattern: /bg-(red|green|blue)-(100|500|900)/ },
'sr-only',
],
theme: {
extend: {
/* Custom tokens are automatically available as JIT classes */
colors: {
brand: {
50: '#f0f9ff',
500: '#0ea5e9',
900: '#0c4a6e',
},
},
/* Fluid typography token, accessible as text-fluid-h1 */
fontSize: {
'fluid-h1': ['clamp(1.75rem, 5vw, 3rem)', { lineHeight: '1.1' }],
},
},
},
plugins: [],
}
One key difference from the classic configuration: in JIT mode, variants no longer need to be explicitly enabled. In Tailwind v2, the configuration required specifying, for every utility, which variants (hover, focus, dark, responsive) should be generated. In the JIT compiler, all variants are automatically available for all utilities; they are only generated when they actually appear in the templates. This drastically reduces configuration complexity and eliminates a common source of errors.
3. Arbitrary Values: Design Tokens Without Configuration
One of the most powerful features of the Tailwind JIT compiler is arbitrary values in square brackets. With the syntax class-name-[value], exact CSS values can be defined directly in HTML without registering them in the configuration beforehand. This is especially valuable for values that come from design specs and do not fit a predefined Tailwind scale: top-[117px], grid-cols-[repeat(auto-fill,minmax(280px,1fr))], bg-[#1a1a2e], or text-[clamp(1rem,3vw,1.5rem)].
Arbitrary values in the Tailwind JIT compiler also allow CSS functions and variables: w-[var(--sidebar-width)] binds a CSS custom property, bg-[url('/img/hero.webp')] sets a background image. With the modifier syntax [&>li]:list-none you can write CSS selectors directly. And with a property hint like [padding:0_20px_0_16px], complete CSS declarations can be defined inline, a bridge between utility-first and raw CSS, without ever leaving the Tailwind structure.
4. Content Configuration: What the JIT Compiler Scans
The content configuration is the heart of the Tailwind JIT compiler. It determines which files the scanner searches to find classes in use. This is also the most common source of errors: if a file is not on the content path, the Tailwind classes used inside it will not be included in the CSS. This may go unnoticed in development if the missing class happens to be covered elsewhere in the scan, but it can be missing entirely in the production build.
The JIT compiler scans files for patterns, not for CSS classes in the strict sense. It looks for contiguous strings that look like Tailwind classes. That means dynamically assembled classes like 'bg-' + color will not be found, because the scanner does not evaluate the JavaScript expression. For dynamically generated classes, you must either store complete class names as strings or use the safelist. This behavior is intentional: the scanner is a simple regex scanner, not a JavaScript interpreter, and that is exactly what makes it so fast.
/* tailwind.config.js: Advanced content configuration */
const path = require('path')
module.exports = {
content: {
/* files: array of glob patterns to scan */
files: [
'./src/**/*.{html,js,ts,jsx,tsx,vue,svelte}',
'./node_modules/@headlessui/vue/dist/**/*.js',
],
/* transform: pre-process file content before scanning */
transform: {
/* Strip PHP tags before scanning, so JIT finds classes in phtml templates */
phtml: (content) => content.replace(/<\?php[^?]*\?>/g, ''),
/* Extract class names from JSON data files */
json: (content) => {
const data = JSON.parse(content)
return Object.values(data).join(' ')
},
},
/* extract: custom extractor for non-standard class formats */
extract: {
/* Alpine.js x-bind:class objects need custom extraction */
js: (content) => {
const classRegex = /['"`]([a-z][a-z0-9-:[\]/.]*)/g
return content.match(classRegex) ?? []
},
},
},
/* ... rest of config */
}
5. Vite Integration: HMR in Under 50 ms
The combination of Tailwind JIT and Vite is the fastest available frontend development stack for CSS-heavy projects. Vite processes PostCSS plugins natively and integrates Tailwind through the official @tailwindcss/vite plugin (from Tailwind v4 onward) or through the PostCSS configuration (Tailwind v3). When a class changes in a template, Vite triggers the Tailwind scanner only for the changed file, generates the delta CSS, and injects it via HMR into the browser, without a page reload. The time between saving and the visible change in the browser is typically under 50 ms.
For Tailwind JIT with Vite, the vite.config.ts configuration is minimal. In Tailwind v4, the plugin integration is enough; the separate tailwind.config.js is no longer needed for simple projects, because theme configuration happens directly in the CSS file via @theme. In v3 you additionally need postcss-import and autoprefixer alongside the Tailwind PostCSS plugin. Both variants benefit from Vite's native ES module graph: only files that changed are reprocessed, so the JIT scan runs incrementally.
/* vite.config.ts: Tailwind JIT with Vite (v4 approach) */
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [
/* Tailwind v4: plugin handles JIT scanning and CSS generation natively */
tailwindcss(),
],
css: {
/* PostCSS is still available for additional plugins */
postcss: {
plugins: [
/* autoprefixer is bundled in Tailwind v4, explicit for v3 */
],
},
},
build: {
/* CSS code splitting: each entry point gets its own minimal CSS chunk */
cssCodeSplit: true,
rollupOptions: {
output: {
/* Deterministic asset filenames for long-term caching */
assetFileNames: 'assets/[name]-[hash][extname]',
},
},
},
})
/* styles/main.css: Entry point for Tailwind v4 */
/* @import 'tailwindcss'; */
/* @theme { */
/* --color-brand-500: #0ea5e9; */
/* --font-sans: 'Inter', sans-serif; */
/* } */
6. PostCSS Setup for Classic Build Pipelines
Not every project uses Vite. In classic Webpack setups, in Laravel Mix, or in standalone PostCSS build scripts, Tailwind JIT is wired in through the PostCSS configuration. The postcss.config.js file registers Tailwind as a PostCSS plugin. The JIT compiler is then automatically active; in Tailwind v3, JIT is the default and a separate mode: 'jit' is no longer needed. In Tailwind v4, the PostCSS configuration is dropped for simple setups in favor of the CSS-native @import "tailwindcss" approach.
The watch configuration is decisive in a PostCSS setup: Tailwind watches the files configured under content using the operating system's native file watcher. On Linux systems with many files, it can become necessary to raise the inotify limit (fs.inotify.max_user_watches), because Tailwind JIT opens a watch descriptor for every content file. In Magento projects with many phtml templates and JavaScript files, this is a well-known configuration issue that a simple sysctl entry resolves.
7. Tailwind JIT in Magento Hyvä
The Hyvä theme for Magento 2 is built entirely on Tailwind CSS and uses the JIT compiler for CSS generation. The build process runs through an NPM script inside the theme's web/tailwind/ directory. The tailwind.config.js scans not only the theme's own template files, but through Hyvä-specific path configuration, also templates from modules that extend the theme. Anyone using custom classes in phtml templates or Alpine.js components must make sure those paths are registered in the content array.
A common problem in Magento Hyvä projects using Tailwind JIT: classes that are dynamically assembled via PHP are missing from the production bundle. One example is a block that reads a CSS class from a CMS block attribute and outputs it in the template as 'bg-' . $block->getData('color'). The JIT compiler will not find this string, because it only scans static character sequences. The solution is the safelist or, better, refactoring the template so that complete class names are stored as static strings in the PHP code. This is cleaner and makes the code more transparent.
8. JIT vs. Classic Build: A Direct Comparison
The differences between Tailwind JIT and the classic pre-build approach are substantial in practice, not just in build time but also in the day-to-day development workflow.
| Aspect | Classic Build (v2) | Tailwind JIT Compiler | JIT Advantage |
|---|---|---|---|
| Dev bundle size | 2-10 MB (all classes) | 5-30 KB (only used) | Fast parsing, no scroll lag in DevTools |
| Build time (dev) | 3-8 s per change | 50-200 ms incremental | Real-time feedback with no waiting |
| Arbitrary values | Not possible without configuration | top-[117px] usable immediately |
Exact design specs with no config overhead |
| Dev vs. prod parity | Different bundles | Identical, same scan | No surprises at deploy time |
| Variant configuration | Manually enabled per utility | All available automatically | Less configuration, fewer mistakes |
The decisive advantage of the Tailwind JIT compiler lies not just in speed, but in the parity between the development and production environments. Because both apply the same scanning algorithm to the same content files, no class that existed in development can be missing in production, as long as all template paths are configured correctly. This parity eliminates an entire class of deploy bugs that regularly occurred with classic Tailwind.
9. Common JIT Problems and Their Solutions
The most common problem with the Tailwind JIT compiler is CSS missing in the production build that was present in development. The cause is almost always a content path configuration that does not cover every file where Tailwind classes appear. Especially tricky: if a class appears in a file on the content path and the same class also appears in a file that is not on the content path, this goes unnoticed in development. Only when you check which file is the actual "source" of the class do you discover the gap.
Another common problem: the JIT compiler does not recognize classes that are dynamically assembled at runtime. Instead of className={`text-${size}`}, you have to store complete classes as an object or array: className={{ 'text-sm': size === 'sm', 'text-lg': size === 'lg' }}. This is not a bug in the JIT compiler, but a fundamental design principle: the scanner is deliberately a simple string matcher that does not evaluate JavaScript expressions. That makes it fast and deterministic, but it requires writing classes as complete, static strings in the code.
/* Debug JIT: check which classes are being scanned */
/* Run this in the project root to see what JIT finds: */
/* npx tailwindcss --content './src/**/*.html' --dry-run */
/* Common fix: ensure all template paths are covered */
module.exports = {
content: [
/* Include node_modules for component libraries */
'./node_modules/@headlessui/**/*.js',
/* Include PHP templates, note the .phtml extension */
'./src/**/*.phtml',
/* Include JS files that contain class strings */
'./src/**/*.{js,ts,jsx,tsx}',
/* Avoid scanning CSS files, leads to false positives */
/* Do NOT include: './src/**/*.css' */
],
/* Safelist for runtime-assembled classes */
safelist: [
/* Pattern-based: include all severity colors */
{ pattern: /^(bg|text|border)-(red|yellow|green)-(100|500|700)$/ },
],
}
/* Fix: split dynamic class names into static complete strings */
/* WRONG, JIT cannot find this: */
/* const cls = `bg-${color}-500` */
/* RIGHT, JIT finds these complete class strings: */
/* const colorMap = { red: 'bg-red-500', blue: 'bg-blue-500' } */
/* const cls = colorMap[color] */
10. Summary
The Tailwind JIT compiler has fundamentally changed the way Tailwind CSS is used in modern projects. Instead of pre-generating a giant stylesheet and purging it afterward, Tailwind JIT scans template files on demand and generates only the CSS that is needed. The result: sub-200-ms builds in development, dev-prod parity without a separate purge configuration, and arbitrary values without any configuration effort. Variant configuration disappears entirely; all modifiers are automatically available.
In practice, this means: configure content paths cleanly, write dynamically assembled classes as complete strings, and use the safelist deliberately for exceptions. In Magento Hyvä projects, the phtml path configuration is especially important. Anyone who follows these points gets, with the Tailwind JIT compiler, a CSS build system that is faster than any alternative and behaves identically in dev mode and in production.
Tailwind JIT Compiler: The Essentials at a Glance
On-demand generation
JIT scans content files and generates only the classes that are used. Dev bundle: 5-30 KB instead of 10 MB. Build times under 200 ms incremental.
Arbitrary values
top-[117px], bg-[#1a2e3b], grid-cols-[repeat(auto-fill,minmax(280px,1fr))], exact values usable immediately, no configuration.
Content configuration
All template paths must be registered in the content array. Dynamic classes must appear as complete static strings in the code.
Dev-prod parity
Same scan algorithm in dev and prod. No PurgeCSS configuration needed. No more classes vanishing on the production deploy.