| |

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

FormMeaning
@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

SyntaxType
true, falseBoolean
123, 0.5Number
Hello worldString (bare word)
'true', '123'String (quoted)
nullNull
a, b, cArray of strings

@plugin vs @utility

Aspect@plugin@utility
PurposeLoad a JavaScript pluginCreate a custom utility
LanguageJavaScriptCSS
SourcePackage or local pathInline in CSS
OptionsSupportedNot applicable
Use caseThird-party extensionsProject-specific utilities

Migration Options

ApproachDirectiveWhen to use
CSS-first@plugin + @theme + @utilityNew projects
Legacy config@configExisting v3 config
Mixed@config + @pluginPartial 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

PitfallWhy It HappensFix
Plugin not loadedWrong package name or pathCheck the name and path
Options not receivedWrong block syntaxUse key-value pairs inside {}
Plugin utilities not purgedPlugin not registered correctlyCheck plugin compatibility
@plugin fails with v3 pluginPlugin not compatible with v4Check for a v4 version
@config not foundWrong pathUse a relative path
Options type mismatchBare word parsed as stringQuote strings that look like other types
Plugin loaded twice@plugin and @config both load itRemove 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

ItemValue
@pluginLoads 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 typesBoolean, number, string, array, null, nested object
@configLoads a legacy tailwind.config.js
@utilityCreates a custom utility in CSS
Migration toolnpx @tailwindcss/upgrade
Compatibilityv3 plugins loaded via compatibility layer

Key takeaways:

  • The @plugin directive loads a JavaScript plugin in v4’s CSS-first configuration. It replaces the plugins array in tailwind.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 @plugin directive is for consuming, and @utility is 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 @config directive loads a legacy tailwind.config.js. It is the backward-compatibility path for projects that have not migrated to CSS-first configuration. The @plugin and @config directives can be used together for partial migrations.
  • The @tailwindcss-upgrade tool automates the migration. 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.
  • 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-500 variable.
  • 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-variant directives replace the JavaScript configuration file. A project that uses these directives does not need a tailwind.config.js at 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!