Tailwind CSS Advanced: Systems, Plugins and Production
Custom Utilities and Directives
In this lesson you will
- Write simple and functional custom utilities with @utility
- Create custom variants and use @variant inside your CSS
- Use the spacing function, prefixes and plugins
Introduction
Tailwind v4 moved extension points into CSS, through directives (special at-rules). This lesson covers the ones you need to customise Tailwind deeply. Every example was compiled with Tailwind 4.3.3, and the results described are the real output.
@utility: simple custom utilities
CSS
@utility tab-4 {
tab-size: 4;
}tab-4 now works like any built-in utility, including with variants (md:tab-4, hover:tab-4). In v3 you added such classes inside @layer utilities. In v4, @utility is the supported way, and it makes sure the class sorts correctly among utilities.
Functional utilities
A utility can accept a value. Name it with a trailing -* and read the value with --value():
CSS
@theme {
--tab-size-github: 8;
}
@utility tab-* {
tab-size: --value(--tab-size-*, integer, [integer]);
}--value() tries the options in order:
- a theme value from the
--tab-size-*namespace - a bare integer
- an arbitrary integer in square brackets
I compiled tab-4, tab-github and tab-[7], and got tab-size: 4, tab-size: var(--tab-size-github) and tab-size: 7 respectively. Functional utilities let you add whole families that match your design system, with automatic support for theme values and arbitrary values.
Quick quiz
What replaces adding custom classes inside @layer utilities in Tailwind v4?
Custom variants
@custom-variant creates your own variant. The selector uses & for the element:
CSS
@custom-variant theme-midnight (&:where([data-theme=midnight] *));After that, theme-midnight:bg-black works. I compiled it and the output was a rule with :where([data-theme=midnight] *) and background-color: var(--color-black). The same directive is how you change dark mode from a media query to a class or data attribute.
For multiple conditions, a variant can wrap rules in an at-rule or use a block form. See the official docs for the full syntax when you need nested conditions.
@variant: variants inside your CSS
In your own CSS you can apply Tailwind variants with @variant:
CSS
.btn {
background: white;
@variant dark {
background: black;
}
@variant hover {
background: gray;
}
}I compiled this to a @media (prefers-color-scheme: dark) block for the dark part and a &:hover rule inside @media (hover: hover) for the hover part, so your handwritten CSS follows the same variant definitions as the utilities.
@apply with variants
@apply accepts variants too. This compiled correctly:
CSS
.btn {
@apply bg-blue-600 hover:bg-blue-700 md:px-6;
}The md:px-6 part became @media (width >= 48rem) { padding-inline: calc(var(--spacing) * 6) }.
Functions
--spacing(4)converts tocalc(var(--spacing) * 4). I compiledmargin: --spacing(4)and got exactly that.var(--color-blue-500)reads any theme variable directly, which is the preferred way to read theme values.- The older
theme()function still exists for compatibility, but the upgrade guide recommends CSS variables. When you must use it, use variable names, such astheme(--breakpoint-xl), not the v3 path styletheme(screens.xl).
Prefixes
If Tailwind classes clash with another system's classes, add a prefix at import time:
CSS
@import "tailwindcss" prefix(tw);Then classes become tw:flex and tw:hover:bg-red-600. In v4 the prefix comes first, like a variant. My compile confirmed tw:flex generated display: flex, and the generated colour variables get the prefix too, such as --tw-color-red-600.
Plugins
For bigger extensions, use @plugin:
CSS
@plugin "@tailwindcss/typography";The official typography plugin adds prose classes that style rendered Markdown. I verified that prose generates nothing without the plugin installed, and the plugin must be added to your project first. Other official and community plugins exist, so evaluate their maintenance and licence before adopting one.
Legacy JavaScript config
v4 does not detect tailwind.config.js automatically. If you must keep one while migrating, load it explicitly:
CSS
@config "../../tailwind.config.js";Options such as corePlugins, safelist and separator are not supported in v4. I tried pointing @config at a missing file and got a clear "Can't resolve" error, which is useful when debugging.
Choosing the right extension
| Need | Use |
|---|---|
| One new class | @utility name |
| A family of classes taking a value | @utility name-* with --value() |
| A new condition like a theme | @custom-variant |
| Plain CSS that follows variants | @variant inside rules |
| Share many styles from CSS | @apply (sparingly) |
| Ship complex features | @plugin |
Try it yourself
Write a functional utility elevation-* that sets box-shadow from a --elevation-* theme namespace, create a theme-midnight custom variant, and compile your CSS to see the output with the Tailwind CLI or your build.
Summary
@utilitydefines custom classes, and@utility name-*with--value()creates functional families.@custom-variantadds conditions,@variantapplies variants in your own CSS, and--spacing()reads the spacing scale.- Use
prefix()to avoid clashes,@pluginfor bigger extensions and@configonly to load a legacy JavaScript config.
Lesson complete
+20 XP
Nice work. Keep going while it is fresh.
Sources and further reading