Web development

Writing a Tailwind plugin

In Tailwind 4 a custom plugin is an @utility rule in CSS. A JavaScript plugin earns its place only when you ship utilities as a package.

Key points

  • @utility adds a custom utility with no JavaScript.
  • @plugin loads a JavaScript plugin, from a package or a local file.
  • safelist and corePlugins do not exist in Tailwind 4.

In Tailwind CSS 4 a custom plugin is an @utility rule in the CSS; a JavaScript plugin earns its place only when you ship it as a package.

The short path: @utility

@import "tailwindcss";

@utility content-auto {
  content-visibility: auto;
  contain-intrinsic-size: 1px 500px;
}

The utility lands in the utilities layer and accepts variants: hover:content-auto, lg:content-auto. No JavaScript, no tailwind.config.js, no package.

A utility with a value

A functional utility takes an argument through --value():

@theme {
  --tab-size-github: 8;
}

@utility tab-* {
  tab-size: --value(--tab-size-*, integer, [integer]);
}

That covers tab-2, tab-76, tab-github and tab-[3]. --value() reads theme keys, bare values and arbitrary values; declarations that do not resolve are left out of the output.

When you need a JavaScript plugin

A JavaScript plugin still earns its place when you publish utilities for other projects. You write it with the plugin() helper:

import plugin from "tailwindcss/plugin";

export default plugin(({ addUtilities, addVariant }) => {
  addUtilities({
    ".content-auto": { "content-visibility": "auto" },
  });
  addVariant("hocus", ["&:hover", "&:focus-visible"]);
});

You load it with the @plugin directive, which takes a package name or a local path. The stable APIs are addUtilities, addComponents and addVariant.

@import "tailwindcss";
@plugin "./hocus-plugin.js";

The corePlugins and safelist options from the JavaScript config are not supported in 4. In place of a safelist there is @source inline().

The limitation

A JavaScript plugin is your code: it has a name, a version and a test to maintain, and its utilities do not exist in a project until someone loads it. In exchange it is untyped, and a wrong selector fails silently. For two internal utilities @utility costs less and stays in the same file.

Where to continue

The catalogue holds Tailwind components, the tailwind-admin library and hero sections, all built from utilities. The framework is explained in what Tailwind CSS is, missing classes in content paths; to pick a library read the best Tailwind libraries and the comparison Tailwind vs CSS Modules.

Frequently asked questions

Do I still need to write JavaScript plugins in Tailwind 4?

Only if you publish utilities for other projects. Inside one application, @utility in the CSS covers almost every case.

How do you load a plugin in Tailwind 4?

With the @plugin directive followed by the package name or a local path. The directive goes in the CSS file that imports tailwindcss.

What is the difference between @utility and addUtilities?

@utility is a CSS rule that the build processes, addUtilities is a function of the JavaScript API. The first stays in the project, the second needs a package.

Can I use a safelist in Tailwind 4?

No, safelist and corePlugins from the JavaScript config are not supported in 4. In their place is @source inline(), which lists the classes to generate.

Can a plugin add a variant?

Yes, with addVariant in the JavaScript API or with @custom-variant in CSS. For a single variant, @custom-variant is the shorter route.