| |

Tailwind CSS 5 🎨 Automatic Content Detection and Template File Scanning

In Tailwind v3, every project needed a content array in tailwind.config.js. That array told Tailwind which files to scan for class names. Forget to update it, and the CSS for a new component would silently disappear from the production build. Tailwind v4 eliminates this configuration entirely. The engine scans the project automatically using a set of heuristics that respect .gitignore and skip binary files. For most projects, the content array is gone for good .

But automatic detection is not magic. It has boundaries. It skips files that Git ignores, which means it skips node_modules. It skips binary extensions. And in monorepos, it may not see shared packages that live outside the app’s root directory. The @source directive is the escape hatch. It lets you add files and directories that the heuristics miss, all from within your CSS file.

Key point: Automatic content detection replaces the content array. The engine scans the project from the CSS file’s location, respecting .gitignore and skipping binary files. The @source directive adds sources that the heuristics miss. The source() function on the @import statement changes the base path or disables automatic detection entirely. The @source inline() and @source not directives, introduced in v4.1, replace the v3 safelist and blocklist options .


Why Automatic Detection Exists

The content array was a source of friction. It required the developer to understand the scanner’s heuristics, to update the array when the project structure changed, and to debug missing classes when a path was wrong.

The configuration burden. A typical v3 content array looked like ['./src/**/*.{html,js,jsx,ts,tsx}']. Every new file type, every new folder, every new package required a configuration update. The array was easy to get wrong, and the failure mode was silent: the class simply did not appear in the CSS .

The failure mode. When Tailwind v3 could not find a class, it did not warn. The class was absent from the generated CSS. The developer saw the class in the markup, saw it missing in the browser, and had to trace the problem back to the content configuration. The debugging cycle was painful.

The heuristics solution. Tailwind v4 scans automatically. It respects .gitignore, so it skips node_modules, build output, and coverage reports without configuration . It skips binary extensions like images, videos, and ZIP files. It scans the files that are under version control, because those are the files that matter.

The trade-off. Automatic detection is convenient, but it is not always complete. A file that is .gitignored but still needs to be scanned — a generated template, a shared UI library in node_modules — must be added with @source. The developer trades configuration for heuristics, and learns the escape hatch when the heuristics fall short.


a. Automatic Detection and the Source Function

Automatic detection starts from the CSS file that contains @import "tailwindcss". The engine treats that file’s directory as the base path and scans everything below it, subject to the .gitignore and binary-extension rules .

/* src/app.css */
@import "tailwindcss";

With this import, Tailwind scans the src/ directory and everything it contains. Files in .gitignore are skipped. Binary files are skipped. The scan is automatic.

The source() function changes the base path. It is useful when the CSS file lives in a nested directory but the project root is elsewhere .

/* src/styles/app.css */
@import "tailwindcss" source("../../../");

The base path is now the project root, three levels up. The scan covers the entire project. This approach can scan too much — it may include directories that are not relevant. The @source not directive can exclude specific paths .

@import "tailwindcss" source("../../../");
@source not "../../../legacy";

The legacy directory is excluded from the scan. The @source not directive is the counterpart to @source and was introduced in v4.1 .

The source(none) option disables automatic detection entirely. The engine scans only the paths specified with @source .

@import "tailwindcss" source(none);
@source "../src";
@source "../shared";

This approach is useful when the automatic scan is too broad or when the project structure requires explicit control. It is the v4 equivalent of an explicit content array.


b. The @source Directive

The @source directive adds a source to the scan. The path is relative to the CSS file that contains the directive .

@import "tailwindcss";
@source "../node_modules/@my-company/ui-lib";

The directive is used for sources that the automatic detection misses. The most common case is a shared package in node_modules. The automatic scan skips node_modules because it is in .gitignore. The @source directive overrides the skip and adds the package to the scan .

The @source directive uses the same heuristics as the automatic detection. It skips binary files. It respects the .gitignore rules for the added directory, unless the directory is explicitly added . This means the directive does not need to list every file extension. It scans the directory and applies the same rules.

In a monorepo, the @source directive bridges the gap between the app and the shared packages. The app’s CSS scans the app’s source and the shared package’s source in a single build. The result is one @layer utilities block containing classes from both packages, which avoids cascade conflicts that would occur if two separate Tailwind builds were imported .

/* apps/webapp/src/app.css */
@import "tailwindcss";
@source "../../packages/shared/src";

The shared package’s classes are now part of the app’s CSS. The build is a single Tailwind build. The classes are not culled.


c. The @source inline() and @source not Directives

Tailwind v4.1 introduced two directives that replace the v3 safelist and blocklist options. They live in the CSS file, alongside the other configuration .

@source inline() forces Tailwind to generate specific classes even when they do not appear in the source files. It is the equivalent of the v3 safelist option .

@source inline("underline");

The underline class is generated even if no source file uses it. The directive supports brace expansion, so a range of classes can be generated in a single line.

@source inline("{hover:,}bg-red-{50,{100..900..100},950}");

This generates bg-red-50, bg-red-100, bg-red-200, through bg-red-900, and bg-red-950, plus the hover: variant for each. The brace expansion is evaluated at build time .

@source not inline() prevents specific classes from being generated, even if they appear in the source files. It is the equivalent of the v3 blocklist option .

@source not inline("container");

The container class is not generated, even if the word container appears in a source file. This is useful when a class name conflicts with a CSS class from another library .

@source not excludes a path from the scan. It was introduced in v4.1 alongside the inline() variant .

@import "tailwindcss";
@source not "./src/components/legacy";

The legacy directory is excluded from the scan. This is useful when a large portion of the project is not relevant to the Tailwind build, or when a directory contains files that cause false positives.

The three directives — @source, @source not, and @source inline() — cover the customization needs that the v3 content, safelist, and blocklist options covered. They live in the CSS file, which keeps the configuration in one place .

v3 Optionv4 DirectivePurpose
content@sourceAdd sources that automatic detection misses
safelist@source inline()Force generation of classes not found in source
blocklist@source not inline()Prevent generation of specific classes

Complete Example Session

This session demonstrates automatic detection, @source, and @source inline() in a Vite React project.

/* ============================================ */
/* PART 1: THE AUTOMATIC DETECTION */
/* ============================================ */

/* src/index.css */
@import "tailwindcss";

/* Tailwind scans the src/ directory automatically.
   Files in .gitignore are skipped.
   Binary files are skipped.
   No content array needed. */

/* ============================================ */
/* PART 2: THE SHARED PACKAGE */
/* ============================================ */

/* The project has a shared UI library in node_modules.
   The library is in .gitignore, so automatic detection skips it.
   The @source directive adds it to the scan. */

@import "tailwindcss";
@source "../node_modules/@my-company/ui-lib";

/* The library's classes are now generated.
   Without this directive, the classes would be missing. */

/* ============================================ */
/* PART 3: THE MONOREPO */
/* ============================================ */

/* apps/webapp/src/app.css */
@import "tailwindcss";
@source "../../packages/shared/src";

/* The shared package's classes are scanned.
   The result is one @layer utilities block.
   No cascade conflicts from separate builds. */

/* ============================================ */
/* PART 4: THE SAFELIST */
/* ============================================ */

/* v4.1 and later */
@import "tailwindcss";
@source inline("underline");
@source inline("bg-red-{50,100..900..100,950}");

/* The classes are generated even if no source file uses them.
   Brace expansion generates the range. */

/* ============================================ */
/* PART 5: THE HOVER VARIANT SAFELIST */
/* ============================================ */

@source inline("{hover:,}bg-red-{50,{100..900..100},950}");

/* Generates bg-red-50, bg-red-100, ..., bg-red-950.
   Also generates hover:bg-red-50, hover:bg-red-100, etc. */

/* ============================================ */
/* PART 6: THE BLOCKLIST */
/* ============================================ */

@source not inline("container");

/* The container class is not generated.
   Even if the word "container" appears in a source file. */

/* ============================================ */
/* PART 7: THE EXCLUDED PATH */
/* ============================================ */

@import "tailwindcss";
@source not "./src/components/legacy";

/* The legacy directory is excluded from the scan. */

/* ============================================ */
/* PART 8: THE DISABLED DETECTION */
/* ============================================ */

@import "tailwindcss" source(none);
@source "../src";
@source "../shared";

/* Automatic detection is disabled.
   Only the paths specified with @source are scanned. */

/* ============================================ */
/* PART 9: THE BASE PATH CHANGE */
/* ============================================ */

/* src/styles/app.css */
@import "tailwindcss" source("../../../");

/* The base path is the project root.
   The scan covers the entire project. */

/* ============================================ */
/* PART 10: THE SCAN RULES */
/* ============================================ */

/* Automatic detection:
   - Respects .gitignore
   - Skips binary extensions
   - Scans from the CSS file's directory */

/* @source:
   - Adds a source
   - Path is relative to the CSS file
   - Uses the same heuristics as automatic detection */

/* @source not:
   - Excludes a path */

/* @source inline():
   - Forces generation of classes
   - Supports brace expansion */

/* @source not inline():
   - Prevents generation of classes */

/* source(none):
   - Disables automatic detection */

/* source(path):
   - Changes the base path */

The ten parts cover automatic detection, the shared package, the monorepo, the safelist, the hover variant safelist, the blocklist, the excluded path, the disabled detection, the base path change, and the scan rules.


Quick Reference

The Source Directives

DirectivePurpose
@sourceAdd a source to the scan
@source notExclude a path from the scan
@source inline()Force generation of classes
@source not inline()Prevent generation of classes

The Source Function

FunctionPurpose
source(path)Change the base path
source(none)Disable automatic detection

The v3 to v4 Mapping

v3 Optionv4 Directive
content@source
safelist@source inline()
blocklist@source not inline()

The Scan Heuristics

RuleBehavior
.gitignoreFiles are skipped
Binary extensionsFiles are skipped
Base pathCSS file’s directory

Best Practices

✅ Do This:

/* Use @source for shared packages */
@source "../node_modules/@my-company/ui-lib";                   /* ✅ */
/* Use @source inline() for dynamic classes */
@source inline("bg-red-{50,100..900..100,950}");                 /* ✅ */
/* Use @source not to exclude legacy paths */
@source not "./src/components/legacy";                           /* ✅ */
/* Use source(none) for explicit control */
@import "tailwindcss" source(none);
@source "../src";                                                /* ✅ */

❌ Don’t Do This:

// Don't use the v3 content array
content: ['./src/**/*.{html,js}']                                /* ❌ */
/* Don't expect automatic detection to scan node_modules */
/* node_modules is in .gitignore                                    /* ❌ */
/* Don't use @source inline() in v4.0 */
/* It was introduced in v4.1                                       /* ❌ */
/* Don't forget the base path for the source() function */
/* The path is relative to the CSS file                           /* ❌ */

Common Pitfalls

PitfallWhy It HappensFix
Classes missing from node_modules.gitignore skips itAdd @source
@source path not workingWrong base pathUse a path relative to the CSS file
Safelist not generatingv4.0 does not support inline()Upgrade to v4.1
Monorepo classes culledShared package not scannedAdd @source for the package
Scan too broadBase path is the project rootUse @source not to exclude

Real-World Examples

1. Automatic Detection

@import "tailwindcss";

2. Shared Package

@source "../node_modules/@my-company/ui-lib";

3. Monorepo Shared Source

@source "../../packages/shared/src";

4. Safelist a Single Class

@source inline("underline");

5. Safelist a Range

@source inline("bg-red-{50,100..900..100,950}");

6. Safelist with Hover

@source inline("{hover:,}bg-red-{50,{100..900..100},950}");

7. Blocklist a Class

@source not inline("container");

8. Exclude a Path

@source not "./src/components/legacy";

9. Disable Detection

@import "tailwindcss" source(none);
@source "../src";

10. Change Base Path

@import "tailwindcss" source("../../../");

Visual

The Automatic Detection Flow

┌──────────────────────────────────────────────┐
│  CSS FILE: src/app.css                       │
│    @import "tailwindcss";                    │
│                                              │
│  SCAN:                                       │
│    Base path = src/                          │
│    ├─ Respect .gitignore                     │
│    ├─ Skip binary files                      │
│    └─ Scan all files in src/                 │
│                                              │
│  RESULT:                                     │
│    CSS generated from class names found      │
│    in the scanned files.                     │
│                                              │
└──────────────────────────────────────────────┘

The @source Directive

┌──────────────────────────────────────────────┐
│  WITHOUT @source:                            │
│    src/ scanned                              │
│    node_modules/ skipped (.gitignore)        │
│    → shared package classes missing          │
│                                              │
│  WITH @source:                               │
│    src/ scanned                              │
│    node_modules/@my-company/ui-lib/ scanned  │
│    → shared package classes generated        │
│                                              │
└──────────────────────────────────────────────┘

The v3 to v4 Mapping

┌──────────────────────────────────────────────┐
│  v3 (tailwind.config.js)                     │
│    content: ['./src/**/*.{html,js}']         │
│    safelist: ['bg-red-500']                  │
│    blocklist: ['container']                  │
│                                              │
│  v4 (CSS file)                               │
│    @import "tailwindcss";                    │
│    @source "../shared";                      │
│    @source inline("bg-red-500");             │
│    @source not inline("container");          │
│                                              │
└──────────────────────────────────────────────┘

The Monorepo Scan

┌──────────────────────────────────────────────┐
│  MONOREPO                                    │
│  ├─ apps/webapp/src/                         │
│  │    └─ app.css                             │
│  │         @import "tailwindcss";            │
│  │         @source "../../packages/shared/src";│
│  └─ packages/shared/src/                     │
│       └─ Button.jsx (uses bg-blue-500)       │
│                                              │
│  The scan covers both directories.           │
│  The result is one @layer utilities block.   │
│                                              │
└──────────────────────────────────────────────┘

Summary

ItemValue
Automatic detectionScans from the CSS file’s directory
.gitignoreFiles are skipped
Binary extensionsFiles are skipped
@sourceAdds a source
@source notExcludes a path
@source inline()Forces generation (v4.1+)
@source not inline()Prevents generation (v4.1+)
source(path)Changes the base path
source(none)Disables automatic detection
v3 contentReplaced by automatic detection and @source
v3 safelistReplaced by @source inline()
v3 blocklistReplaced by @source not inline()

Key takeaways:

  • Automatic content detection replaces the content array. Tailwind v4 scans the project from the CSS file’s directory. It respects .gitignore and skips binary files. For most projects, no configuration is needed .
  • The @source directive adds sources that the automatic detection misses. The most common case is a shared package in node_modules. The path is relative to the CSS file. The directive uses the same heuristics as automatic detection .
  • The source() function changes the base path or disables detection. source(path) rebases the scan to a different directory. source(none) disables automatic detection, leaving only the paths specified with @source .
  • The @source inline() directive is the v4.1 replacement for safelist. It forces Tailwind to generate classes that do not appear in the source files. It supports brace expansion for generating ranges of classes .
  • The @source not and @source not inline() directives exclude paths and classes. @source not excludes a directory from the scan. @source not inline() prevents specific classes from being generated, replacing the v3 blocklist option .
  • In a monorepo, @source bridges the gap between the app and the shared packages. The app’s CSS scans both the app’s source and the shared package’s source in a single build. This avoids the cascade conflicts that would occur with two separate Tailwind builds .
  • The @source inline() directive must be quoted. The error message '@source' paths must be quoted appears when the path is not wrapped in quotes. This is a common pitfall when upgrading from v4.0 to v4.1 .

Remember: Tailwind v4 scans automatically. The content array is gone. The engine respects .gitignore and skips binary files. The @source directive adds sources that the heuristics miss. The source() function changes the base path or disables detection. The @source inline() directive replaces the safelist. The @source not directive replaces the blocklist. The configuration lives in the CSS file. The automatic detection is the default. The @source directive is the escape hatch.


Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!