Tailwind CSS 45 🎨 Extension via Native CSS Plugins (@plugin Directive)
The @plugin directive is Tailwind v4’s mechanism for loading JavaScript-based plugins in a CSS-first configuration. In v3, plugins were registered in the plugins array of tailwind.config.js. In v4, the configuration moves to CSS, and the @plugin directive is the replacement. It loads a plugin from a package name or a local path, and it can pass options to the plugin using a nested block syntax. The directive is the bridge between the legacy JavaScript plugin ecosystem and the new CSS-first configuration model.
The distinction between @utility and @plugin is the distinction between authoring and consuming. The @utility directive is for creating custom utilities directly in CSS. The @plugin directive is for loading an existing plugin that was authored in JavaScript, often published to npm, and distributed as a package. A plugin like @tailwindcss/typography is not something a developer rewrites in CSS; it is a dependency that is loaded with @plugin and configured with options. The two directives serve different purposes: @utility is for custom code in the project, and @plugin is for third-party extensions.
The option syntax is the part that is most often overlooked. A plugin that accepts a configuration object receives it through the @plugin block. The @tailwindcss/typography plugin, for example, accepts a className option that renames the prose class to something else. The option is written as a key-value pair inside the @plugin block, and the plugin’s JavaScript receives it as the second argument to the plugin function. The syntax supports strings, numbers, Booleans, null, arrays, and nested objects, and the types are preserved when the plugin receives the options.
This chapter covers three areas. First, why @plugin exists — the problem of loading JavaScript plugins in a CSS-first configuration and the relationship between @plugin and @utility. Second, how to load plugins and pass options — the package name and local path forms, the option block syntax, and the type system for plugin options. Third, how to migrate from v3 and when to use @plugin versus @utility — the @config directive for legacy configurations, the compatibility layer for v3 plugins, and the decision criteria for authoring a plugin versus writing a custom utility. The chapter ends with a complete example session, a quick reference, best practices, common pitfalls, real-world examples, and diagrams showing the plugin loading flow.
Key point: The @plugin directive loads a JavaScript-based plugin in Tailwind v4’s CSS-first configuration. It accepts a package name or a local path, and it passes options to the plugin using a nested block syntax. Use @plugin to load existing plugins like @tailwindcss/typography, and use @utility to create custom utilities directly in CSS. The @config directive loads a legacy tailwind.config.js for backward compatibility.
Why @plugin exists
The plugin ecosystem problem. Tailwind has a large ecosystem of JavaScript plugins: @tailwindcss/typography, @tailwindcss/forms, daisyui, tailwind-scrollbar, and many others. These plugins were written for v3’s JavaScript configuration, and they register utilities, variants, and base styles through the plugin API. In v4, the configuration is CSS-first, and the plugins array in tailwind.config.js is no longer the default. The @plugin directive is the v4 replacement. It loads the same plugins, passes the same options, and registers the same utilities, without requiring a JavaScript configuration file.
The CSS-first problem. The @plugin directive is part of the CSS-first configuration model. The @import "tailwindcss" statement replaces the @tailwind directives, the @theme directive replaces the theme key, and the @plugin directive replaces the plugins array. A project that uses @plugin does not need a tailwind.config.js file at all. The entire configuration lives in the CSS file: the theme, the plugins, the custom utilities, and the custom variants. The @config directive is available for projects that still need a JavaScript configuration, but it is not the default.
The option-passing problem. A plugin often accepts options. The @tailwindcss/typography plugin accepts a className option, and the fluid-tailwindcss plugin accepts a variables map. In v3, the options were passed as arguments to the plugin function in the plugins array: plugins: [require('@tailwindcss/typography')({ className: 'wysiwyg' })]. In v4, the options are passed through the @plugin block. The syntax is a CSS-like key-value block, and the plugin’s JavaScript receives the values with their types preserved.
The @utility-vs-@plugin problem. The two directives are not interchangeable. The @utility directive creates a custom utility in the project’s CSS. The @plugin directive loads a plugin authored in JavaScript. A plugin that generates a family of utilities from a configuration object cannot be rewritten as a single @utility. A plugin that registers base styles, custom variants, and utilities together cannot be rewritten as a single @utility. The @plugin directive is for the cases where the plugin’s logic is more complex than a static utility definition.
The migration problem. A project migrating from v3 to v4 has three options. First, migrate the tailwind.config.js to CSS using @theme, @utility, and @custom-variant. Second, keep the tailwind.config.js and load it with @config. Third, load the individual plugins with @plugin and leave the rest of the configuration in the JavaScript file. The @tailwindcss-upgrade tool automates the migration, extracting static plugins into @plugin directives and converting the theme and content paths. For complex plugins that cannot be statically migrated, the compatibility layer handles the registration of legacy utilities and the loading of JS-based plugins.
The trade-off. The @plugin directive depends on the plugin being compatible with v4. A v3 plugin that uses the legacy plugin API is loaded through a compatibility layer, and some plugins may not work correctly. The plugin’s documentation should be checked for v4 support. A plugin that is not compatible must either be updated or replaced with a CSS-first implementation. The trade-off is between the convenience of the existing plugin ecosystem and the compatibility of the new configuration model.
a. Loading a plugin
The @plugin directive takes a package name or a local path. The package name is resolved from node_modules, and the local path is resolved relative to the CSS file.
@import "tailwindcss";
@plugin "@tailwindcss/typography";
The @tailwindcss/typography plugin is loaded and its utilities are registered. The plugin adds the prose class and its variants to the project. The prose class can be used in the markup, and it is purged when unused.
A local plugin is loaded with a relative path:
@plugin "./tailwind.heroicons.js";
The path is relative to the CSS file that contains the directive. The plugin file is a JavaScript module that exports the plugin function, either as a default export or as a named export. The @plugin directive loads the module and calls the plugin function.
A plugin that has no default export may need to be loaded with the .js extension or with a specific export name. The @plugin directive resolves the default export by default, and it falls back to the module’s exports if the default is not present.
b. Passing options to a plugin
The @plugin directive accepts an option block using a CSS-like syntax. The block is written after the plugin name, and the key-value pairs are passed to the plugin function as a single object.
@plugin "@tailwindcss/typography" {
className: wysiwyg;
}
The className option is passed to the @tailwindcss/typography plugin, and the plugin renames the prose class to wysiwyg. The option value is a bare word, and the plugin receives it as a string.
The option syntax supports multiple types. A Boolean, a number, a string, an array, and a null value are all supported:
@plugin "my-plugin" {
debug: false;
threshold: 0.5;
message: Hello world;
features: base, responsive;
is-null: null;
}
The plugin receives debug as the boolean false, threshold as the number 0.5, message as the string "Hello world", features as the array ["base", "responsive"], and is-null as null. The types are preserved, and the plugin’s JavaScript can use them directly.
A string that looks like a Boolean or a number can be quoted to preserve its type:
@plugin "my-plugin" {
is-str-true: 'true';
is-str-int: '123';
}
The plugin receives the strings "true" and "123", not the boolean true and the number 123. The quotes are the signal that the value is a string, not a bare word.
A nested object is written with a nested block:
@plugin "fluid-tailwindcss" {
minViewport: 320;
maxViewport: 1920;
useRem: true;
}
The plugin receives an object with minViewport, maxViewport, and useRem as properties. The nested block is the object, and the key-value pairs are its properties.
A plugin that expects an array of strings receives a comma-separated list:
@plugin "my-plugin" {
features: base, responsive, dark;
}
The plugin receives ["base", "responsive", "dark"]. The comma is the array separator, and the values are the elements.
c. Migration and the @config directive
The @config directive loads a legacy tailwind.config.js file. It is the backward-compatibility path for projects that have a JavaScript configuration and do not want to migrate it to CSS.
@import "tailwindcss";
@config "./tailwind.config.js";
The configuration file is loaded, and its theme, plugins, and content settings are applied. The content setting is not needed in v4 because of automatic source detection, but the @source directive can be used to supplement it. The plugins array in the config file is loaded, and the plugins are registered. The @plugin directive can be used alongside @config, so a project can load some plugins with @plugin and keep others in the JavaScript file.
The @tailwindcss-upgrade tool automates the migration from v3 to v4. It converts the tailwind.config.js into CSS, extracting static plugins into @plugin directives and converting the theme keys into @theme variables. The content paths are converted to @source directives. For plugins that cannot be statically migrated, the tool leaves them in the JavaScript file and adds a @config directive.
The compatibility layer handles the registration of legacy utilities and the loading of JS-based plugins. The applyCompatibilityHooks function upgrades the design system’s theme resolution to support the legacy dot-notation paths that v3 plugins use. This means a v3 plugin that reads theme('colors.red.500') still works, even though the theme is now defined in @theme with the --color-red-500 variable.
Complete Example Session
/* ============================================
PART 1: A BASIC PLUGIN LOAD
============================================ */
@import "tailwindcss";
@plugin "@tailwindcss/typography";
/* The typography plugin is loaded.
The prose class and its variants are available.
<article class="prose"> ... </article> */
/* ============================================
PART 2: A PLUGIN WITH OPTIONS
============================================ */
@plugin "@tailwindcss/typography" {
className: wysiwyg;
}
/* The prose class is renamed to wysiwyg.
<article class="wysiwyg"> ... </article> */
/* ============================================
PART 3: A LOCAL PLUGIN
============================================ */
@plugin "./tailwind.heroicons.js";
/* The plugin is loaded from a local path.
The path is relative to the CSS file. */
/* ============================================
PART 4: OPTION TYPES
============================================ */
@plugin "my-plugin" {
debug: false;
threshold: 0.5;
message: Hello world;
features: base, responsive;
is-null: null;
}
/* The plugin receives:
debug: false (boolean)
threshold: 0.5 (number)
message: "Hello world" (string)
features: ["base", "responsive"] (array)
is-null: null */
/* ============================================
PART 5: STRING QUOTING
============================================ */
@plugin "my-plugin" {
is-str-true: 'true';
is-str-int: '123';
}
/* The plugin receives strings, not booleans or numbers. */
/* ============================================
PART 6: A NESTED OBJECT
============================================ */
@plugin "fluid-tailwindcss" {
minViewport: 320;
maxViewport: 1920;
useRem: true;
}
/* The plugin receives an object with the three properties. */
/* ============================================
PART 7: THE @config DIRECTIVE
============================================ */
@import "tailwindcss";
@config "./tailwind.config.js";
/* The legacy config file is loaded.
The theme, plugins, and content settings are applied. */
/* ============================================
PART 8: MIXING @plugin AND @config
============================================ */
@import "tailwindcss";
@config "./tailwind.config.js";
@plugin "@tailwindcss/typography";
/* Some plugins are loaded with @plugin.
Others remain in the config file's plugins array. */
/* ============================================
PART 9: THE MIGRATION PATH
============================================ */
/* v3 tailwind.config.js:
module.exports = {
plugins: [require('@tailwindcss/typography')],
};
*/
/* v4 CSS:
@plugin "@tailwindcss/typography";
*/
/* The @tailwindcss-upgrade tool automates this conversion. */
/* ============================================
PART 10: THE COMPLETE STYLESHEET
============================================ */
@import "tailwindcss";
@plugin "@tailwindcss/typography" {
className: prose;
}
@plugin "@tailwindcss/forms";
@plugin "./tailwind.heroicons.js";
@theme {
--color-brand-500: oklch(0.6 0.2 250);
}
@utility content-auto {
content-visibility: auto;
}
/* The complete stylesheet: plugins, theme, and utilities
in a single CSS file. No tailwind.config.js needed. */
The ten parts show a basic plugin load, a plugin with options, a local plugin, option types, string quoting, a nested object, the @config directive, mixing @plugin and @config, the migration path, and the complete stylesheet.
Quick Reference
@plugin Syntax
| Form | Meaning |
|---|---|
@plugin "package-name"; | Load a plugin from npm |
@plugin "./local-plugin.js"; | Load a local plugin |
@plugin "name" { key: value; } | Load with options |
@plugin "name" { key: value1, value2; } | Array option |
@plugin "name" { nested: { key: value; } } | Nested object |
Option Types
| Syntax | Type |
|---|---|
true, false | Boolean |
123, 0.5 | Number |
Hello world | String (bare word) |
'true', '123' | String (quoted) |
null | Null |
a, b, c | Array of strings |
@plugin vs @utility
| Aspect | @plugin | @utility |
|---|---|---|
| Purpose | Load a JavaScript plugin | Create a custom utility |
| Language | JavaScript | CSS |
| Source | Package or local path | Inline in CSS |
| Options | Supported | Not applicable |
| Use case | Third-party extensions | Project-specific utilities |
Migration Options
| Approach | Directive | When to use |
|---|---|---|
| CSS-first | @plugin + @theme + @utility | New projects |
| Legacy config | @config | Existing v3 config |
| Mixed | @config + @plugin | Partial migration |
Best Practices
✅ Do This:
/* Load third-party plugins with @plugin */
@plugin "@tailwindcss/typography"; // ✅
/* Pass options in the block */
@plugin "@tailwindcss/typography" { className: wysiwyg; } // ✅
/* Use @utility for project-specific utilities */
@utility content-auto { content-visibility: auto; } // ✅
/* Use @config for legacy configurations */
@config "./tailwind.config.js"; // ✅
/* Use the @tailwindcss-upgrade tool for migration */
/* npx @tailwindcss/upgrade */ // ✅
❌ Don’t Do This:
/* Don't use @plugin for a custom utility */
@plugin "./my-utility.js" // use @utility instead // ❌
/* Don't forget the semicolon after the plugin name */
@plugin "@tailwindcss/typography" // missing ; // ❌
/* Don't pass options as a JSON string */
@plugin "my-plugin" { options: '{"debug":true}' } // use block syntax // ❌
/* Don't mix @config and @plugin for the same plugin */
@config "./tailwind.config.js"; @plugin "same-plugin"; // ❌
/* Don't ignore plugin compatibility */
@plugin "v3-only-plugin"; // check for v4 support // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Plugin not loaded | Wrong package name or path | Check the name and path |
| Options not received | Wrong block syntax | Use key-value pairs inside {} |
| Plugin utilities not purged | Plugin not registered correctly | Check plugin compatibility |
@plugin fails with v3 plugin | Plugin not compatible with v4 | Check for a v4 version |
@config not found | Wrong path | Use a relative path |
| Options type mismatch | Bare word parsed as string | Quote strings that look like other types |
| Plugin loaded twice | @plugin and @config both load it | Remove one |
Real-World Examples
1. Typography Plugin
@plugin "@tailwindcss/typography";
2. Typography with Custom Class
@plugin "@tailwindcss/typography" { className: wysiwyg; }
3. Forms Plugin
@plugin "@tailwindcss/forms";
4. Local Plugin
@plugin "./tailwind.heroicons.js";
5. Plugin with Options
@plugin "fluid-tailwindcss" { minViewport: 320; maxViewport: 1920; }
6. Array Option
@plugin "my-plugin" { features: base, responsive; }
7. Legacy Config
@config "./tailwind.config.js";
8. Mixed Configuration
@config "./tailwind.config.js"; @plugin "@tailwindcss/typography";
9. Migration
npx @tailwindcss/upgrade
10. Complete Configuration
@import "tailwindcss";
@plugin "@tailwindcss/typography";
@theme { --color-brand-500: oklch(0.6 0.2 250); }
Visual
The Plugin Loading Flow
┌──────────────────────────────────────────────────────────────┐
│ PLUGIN LOADING FLOW │
│ │
│ CSS file: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @import "tailwindcss"; │ │
│ │ @plugin "@tailwindcss/typography" { │ │
│ │ className: wysiwyg; │ │
│ │ } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ Tailwind resolves the package: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ node_modules/@tailwindcss/typography/ │ │
│ │ → loads the plugin module │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The plugin function is called with options: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ plugin({ className: 'wysiwyg' }) │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ The plugin registers its utilities: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ .wysiwyg { ... } │ │
│ │ .wysiwyg-lg { ... } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
@plugin vs @utility
┌──────────────────────────────────────────────────────────────┐
│ @plugin vs @utility │
│ │
│ @plugin: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @plugin "@tailwindcss/typography"; │ │
│ │ │ │
│ │ Loads a JavaScript plugin from a package. │ │
│ │ The plugin registers its own utilities. │ │
│ │ The project does not write the CSS. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ @utility: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @utility content-auto { │ │
│ │ content-visibility: auto; │ │
│ │ } │ │
│ │ │ │
│ │ Creates a custom utility in the project's CSS. │ │
│ │ The project writes the CSS. │ │
│ │ No JavaScript involved. │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ @plugin is for consuming. @utility is for authoring. │
│ │
└──────────────────────────────────────────────────────────────┘
Option Syntax
┌──────────────────────────────────────────────────────────────┐
│ OPTION SYNTAX │
│ │
│ @plugin "my-plugin" { │
│ debug: false; ← boolean │
│ threshold: 0.5; ← number │
│ message: Hello world; ← string (bare word) │
│ features: base, responsive; ← array │
│ is-null: null; ← null │
│ nested: { ← nested object │
│ key: value; │
│ } │
│ } │
│ │
│ The plugin receives: │
│ { │
│ debug: false, │
│ threshold: 0.5, │
│ message: "Hello world", │
│ features: ["base", "responsive"], │
│ is-null: null, │
│ nested: { key: "value" } │
│ } │
│ │
└──────────────────────────────────────────────────────────────┘
Migration Path
┌──────────────────────────────────────────────────────────────┐
│ MIGRATION PATH │
│ │
│ v3 tailwind.config.js: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ module.exports = { │ │
│ │ plugins: [ │ │
│ │ require('@tailwindcss/typography'), │ │
│ │ require('@tailwindcss/forms'), │ │
│ │ ], │ │
│ │ theme: { extend: { colors: { brand: '...' } } } │ │
│ │ }; │ │
│ └──────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ npx @tailwindcss/upgrade │
│ │
│ v4 CSS: │
│ ┌──────────────────────────────────────────────────────┐ │
│ │ @import "tailwindcss"; │ │
│ │ @plugin "@tailwindcss/typography"; │ │
│ │ @plugin "@tailwindcss/forms"; │ │
│ │ @theme { --color-brand: ...; } │ │
│ └──────────────────────────────────────────────────────┘ │
│ │
│ The tool converts the JS config to CSS directives. │
│ │
└──────────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
@plugin | Loads a JavaScript-based plugin in CSS-first config |
| Package form | @plugin "package-name"; |
| Local form | @plugin "./path/to/plugin.js"; |
| Options block | @plugin "name" { key: value; } |
| Option types | Boolean, number, string, array, null, nested object |
@config | Loads a legacy tailwind.config.js |
@utility | Creates a custom utility in CSS |
| Migration tool | npx @tailwindcss/upgrade |
| Compatibility | v3 plugins loaded via compatibility layer |
Key takeaways:
- The
@plugindirective loads a JavaScript plugin in v4’s CSS-first configuration. It replaces thepluginsarray intailwind.config.js. The directive accepts a package name or a local path and registers the plugin’s utilities, variants, and base styles. - Options are passed through a nested block. The block syntax is CSS-like:
@plugin "name" { key: value; }. The plugin’s JavaScript receives the options as a single object, with the types preserved. Booleans, numbers, strings, arrays, and null values are all supported. - The
@plugindirective is for consuming, and@utilityis for authoring. A plugin is a JavaScript package that registers its own utilities. A custom utility is CSS written in the project. The two directives serve different purposes and are not interchangeable. - The
@configdirective loads a legacytailwind.config.js. It is the backward-compatibility path for projects that have not migrated to CSS-first configuration. The@pluginand@configdirectives can be used together for partial migrations. - The
@tailwindcss-upgradetool automates the migration. It converts thetailwind.config.jsinto CSS, extracting static plugins into@plugindirectives and converting the theme keys into@themevariables. The content paths are converted to@sourcedirectives. - A compatibility layer handles v3 plugins. The layer upgrades the design system’s theme resolution to support the legacy dot-notation paths that v3 plugins use. A v3 plugin that reads
theme('colors.red.500')still works with the--color-red-500variable. - Plugin compatibility must be checked. A v3 plugin may not work correctly with v4, even with the compatibility layer. The plugin’s documentation should be checked for v4 support, and a plugin that is not compatible must be updated or replaced.
- The complete configuration lives in CSS. The
@import,@plugin,@theme,@utility, and@custom-variantdirectives replace the JavaScript configuration file. A project that uses these directives does not need atailwind.config.jsat all.
Remember: The @plugin directive is the bridge between the legacy JavaScript plugin ecosystem and Tailwind v4’s CSS-first configuration. It loads a plugin, passes options, and registers the plugin’s utilities. Use it for third-party extensions like @tailwindcss/typography and @tailwindcss/forms. Use @utility for custom utilities written in the project. Use @config for legacy configurations that have not been migrated. The migration tool converts the JavaScript configuration into CSS directives, and the compatibility layer handles the v3 plugins. The result is a single CSS file that contains the entire configuration: the theme, the plugins, the custom utilities, and the custom variants.
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!