v3 content:[] → v4 @source
Tailwind ships only the CSS for classes it can see in your source.
v4 finds those files automatically (plain-text scan from the stylesheet's base
path). @source is the CSS-first override — it lives in your
app.css, not in tailwind.config.js. The Play CDN
mirrors this idea at runtime: it scans the live DOM via a
MutationObserver and compiles whatever class tokens it finds.
| concept | v3 (config file) | v4 (CSS-first) |
|---|---|---|
| where it lives | tailwind.config.js → content: [...] |
your CSS → @source "..."; |
| default behavior | you MUST list every path | auto-detect: scans everything from the base path, skips .gitignore/node_modules/binaries/CSS/lockfiles |
| add a path | content: ["./src/**/*.{tsx,jsx}"] |
@source "../src/components"; (relative to the stylesheet) |
| exclude a path | glob negation in the array | @source not "../legacy"; |
| safelist (force-generate) | safelist: [...] |
@source inline("..."); with brace expansion |
| kill auto-detection | no auto-detection to kill | @import "tailwindcss" source(none); |
| set the base path | content: paths are the base |
@import "tailwindcss" source("../src"); |
1 · simulate the build-time scan
This is a build-time concept — nothing ships to the browser. Toggle the
files below to model @source (include) vs @source not
(exclude). The readout shows which class tokens Tailwind would extract by
scanning those files as plain text, and the unique utilities it would
try to generate. Flip source(none) to model disabling
auto-detection (then nothing is auto-included).
clearfix, float-left) are
extracted as candidates but thrown away — Tailwind never parses your code as JS/HTML,
it only hunts for class-like strings.
2 · @source inline() proven live (safelist analog)
At build time, @source inline("bg-fuchsia-500") forces Tailwind to
emit a class even if it never appears literally in any file. The Play CDN
can't read @source — instead it watches the DOM with a
MutationObserver. So we prove the same promise live:
click inject to add bg-fuchsia-500 to the box below after
load. The CDN spots the new token and compiles it — exactly what a safelist
guarantees.
auto-detected (present at load)
safelist-injected (added post-load)
static: — · dynamic: —
intent → @source pattern
| intent | pattern | why |
|---|---|---|
| scan a gitignored dep | @source "../node_modules/@acme/ui"; |
node_modules is skipped by default — a Tailwind-built lib lives there |
| monorepo: scan a sibling package | @source "../../packages/shared"; |
auto-detection stays inside the base path; reach across with a relative path |
| monorepo: set the base path | @import "tailwindcss" source("../src"); |
build runs from repo root; point detection at this app's src |
| exclude legacy / non-Tailwind dir | @source not "../legacy"; |
large dirs with zero Tailwind classes just slow the scan |
| turn auto-detection off | @import "tailwindcss" source(none); |
multi-stylesheet apps: each CSS file gets only its own explicit @sources |
| safelist one class | @source inline("underline"); |
force-generate a class that no file contains literally (CMS, dynamic) |
| safelist a grid of classes | @source inline("{hover:,}bg-red-{100,500,900}"); |
brace expansion (comma = set, {a..z..step} = range) |
| ban a class outright | @source not inline("container"); |
detected in files but you never want it emitted |