Tailwind Plugins
Writing custom Tailwind CSS plugins to add utilities, components, base styles, and variants
You are an expert in extending Tailwind CSS by writing custom plugins.
## Key Points
- **Use `theme()` to reference config values.** Never hardcode colors or spacing in plugin code; always pull from the theme so users can customize.
- **Prefer `matchUtilities` for value-accepting utilities.** It gives users arbitrary value support (`[...]`) for free and integrates with the theme.
- **Separate plugins into files** when they grow beyond a few lines or when they need to be shared across projects.
- **Use `addComponents` for multi-property classes and `addUtilities` for single-property classes.** This affects how they interact with `@apply` and specificity.
- **Document your plugin's classes.** Custom plugins are invisible to IDE tooling unless you also provide a configuration for the Tailwind CSS IntelliSense extension.
- **Test with PurgeCSS.** Ensure your plugin-generated classes are included in the final output by verifying content scanning picks them up.
- **Forgetting to wrap the plugin with `require('tailwindcss/plugin')`.** Raw functions do not work; you must use the `plugin()` wrapper.
- **Using CSS property names instead of camelCase.** Inside plugin JavaScript, use `backgroundColor` not `background-color`. Tailwind converts them to CSS.
- **Not handling the `e()` escape function for dynamic class names.** If you generate class names from user input (e.g., color names with slashes), use `e()` to properly escape them.
- **Overusing plugins for things `@apply` handles.** If you only need a shorthand for a few utilities, `@apply` in a CSS file is simpler than a plugin.
## Quick Example
```js
// tailwind.config.js
plugins: [
require('./plugins/container-queries')({ prefix: 'cq-' }),
]
```skilldb get tailwind-skills/Tailwind PluginsFull skill: 355 linesWriting Tailwind Plugins — Tailwind CSS
You are an expert in extending Tailwind CSS by writing custom plugins.
Overview
Tailwind's plugin system lets you register new utilities, components, base styles, and variants using JavaScript. Plugins have full access to Tailwind's configuration and can generate CSS programmatically. This is the right tool when you need design-system-level additions that go beyond what @apply or arbitrary values can achieve.
Core Philosophy
Tailwind plugins are the extension mechanism for when the built-in utility set and configuration are genuinely insufficient. They are not a replacement for arbitrary values, @apply, or configuration — they are for design-system-level additions that need to generate CSS programmatically, respond to theme values, or introduce new variants that do not exist in core. The bar for writing a plugin should be higher than "I want a shortcut."
A well-written plugin feels native to Tailwind. It pulls values from theme() so users can customize it through their config, it supports arbitrary value syntax via matchUtilities, and it respects the utility-versus-component distinction. Utilities are single-purpose and composable; components are multi-property and opinionated. Mixing these up — putting complex multi-property styles in addUtilities or single-property helpers in addComponents — creates confusing specificity behavior and breaks the mental model.
Plugins should be testable, documented, and separated into their own files when they grow beyond a few lines. A plugin buried inside tailwind.config.js as an anonymous function is easy to overlook and hard to share. Extracting it into a standalone file with plugin() or plugin.withOptions() makes it portable across projects and discoverable by other developers.
Core Concepts
Plugin API
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(function ({ addUtilities, addComponents, addBase, addVariant, matchUtilities, theme, e }) {
// Plugin logic here
}),
],
}
Key functions available inside the plugin callback:
| Function | Purpose |
|---|---|
addUtilities | Register new utility classes |
addComponents | Register component classes |
addBase | Add base/reset styles |
addVariant | Create custom variants |
matchUtilities | Create dynamic utilities that accept values from the theme |
theme | Access resolved theme values |
e | Escape class names |
Implementation Patterns
Adding Simple Utilities
const plugin = require('tailwindcss/plugin')
module.exports = {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
'.text-balance': {
'text-wrap': 'balance',
},
'.text-pretty': {
'text-wrap': 'pretty',
},
'.content-auto': {
'content-visibility': 'auto',
},
'.scrollbar-hidden': {
'-ms-overflow-style': 'none',
'scrollbar-width': 'none',
'&::-webkit-scrollbar': {
display: 'none',
},
},
})
}),
],
}
Usage: <p class="text-balance">Balanced heading text</p>
Dynamic Utilities with matchUtilities
matchUtilities creates utilities that accept values from the theme, supporting arbitrary values:
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
'animation-delay': (value) => ({
'animation-delay': value,
}),
},
{
values: theme('transitionDelay'),
}
)
})
Usage: animation-delay-150, animation-delay-300, animation-delay-[400ms]
// Grid area utility
plugin(function ({ matchUtilities }) {
matchUtilities(
{
'grid-area': (value) => ({
'grid-area': value,
}),
},
{
values: {
header: 'header',
sidebar: 'sidebar',
main: 'main',
footer: 'footer',
},
}
)
})
Adding Component Styles
plugin(function ({ addComponents, theme }) {
addComponents({
'.btn': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
borderRadius: theme('borderRadius.md'),
padding: `${theme('spacing.2')} ${theme('spacing.4')}`,
fontSize: theme('fontSize.sm[0]'),
fontWeight: theme('fontWeight.semibold'),
lineHeight: theme('fontSize.sm[1].lineHeight'),
transition: 'all 150ms ease-in-out',
'&:focus-visible': {
outline: '2px solid transparent',
outlineOffset: '2px',
boxShadow: `0 0 0 2px ${theme('colors.white')}, 0 0 0 4px ${theme('colors.indigo.600')}`,
},
},
'.btn-primary': {
backgroundColor: theme('colors.indigo.600'),
color: theme('colors.white'),
'&:hover': {
backgroundColor: theme('colors.indigo.500'),
},
},
'.btn-secondary': {
backgroundColor: theme('colors.white'),
color: theme('colors.gray.900'),
border: `1px solid ${theme('colors.gray.300')}`,
'&:hover': {
backgroundColor: theme('colors.gray.50'),
},
},
})
})
Adding Base Styles
plugin(function ({ addBase, theme }) {
addBase({
'h1': {
fontSize: theme('fontSize.3xl[0]'),
fontWeight: theme('fontWeight.bold'),
lineHeight: theme('fontSize.3xl[1].lineHeight'),
},
'h2': {
fontSize: theme('fontSize.2xl[0]'),
fontWeight: theme('fontWeight.semibold'),
lineHeight: theme('fontSize.2xl[1].lineHeight'),
},
'a': {
color: theme('colors.indigo.600'),
textDecoration: 'underline',
textUnderlineOffset: '2px',
'&:hover': {
color: theme('colors.indigo.500'),
},
},
})
})
Custom Variants
plugin(function ({ addVariant }) {
// Matches when a parent has a specific data attribute
addVariant('data-active', '&[data-active="true"]')
addVariant('group-data-active', ':merge(.group)[data-active="true"] &')
// Matches based on an ancestor state
addVariant('sidebar-open', '.sidebar-open &')
addVariant('sidebar-closed', '.sidebar-closed &')
// Matches a child selector
addVariant('hocus', ['&:hover', '&:focus'])
// Not-last-child
addVariant('not-last', '&:not(:last-child)')
// Supports complex selectors
addVariant('peer-invalid-visible', ':merge(.peer):invalid:not(:placeholder-shown) ~ &')
})
Usage:
<div data-active="true" class="data-active:bg-blue-100 data-active:text-blue-900">
Active item
</div>
<button class="hocus:bg-gray-100">Hover or focus</button>
<div class="sidebar-open:translate-x-0 sidebar-closed:-translate-x-full">
Sidebar content
</div>
Standalone Plugin File
For reusable plugins, create a separate file:
// plugins/typography-extras.js
const plugin = require('tailwindcss/plugin')
module.exports = plugin(
function ({ addUtilities, matchUtilities, theme }) {
addUtilities({
'.text-balance': { 'text-wrap': 'balance' },
'.text-pretty': { 'text-wrap': 'pretty' },
})
matchUtilities(
{
'text-shadow': (value) => ({ textShadow: value }),
},
{
values: {
sm: '0 1px 2px rgba(0,0,0,0.1)',
DEFAULT: '0 2px 4px rgba(0,0,0,0.1)',
lg: '0 4px 8px rgba(0,0,0,0.15)',
none: 'none',
},
}
)
},
{
// Optional: extend the user's theme with defaults
theme: {
extend: {
// These become defaults the user can override
},
},
}
)
// tailwind.config.js
module.exports = {
plugins: [
require('./plugins/typography-extras'),
],
}
Plugin with Options
// plugins/container-queries.js
const plugin = require('tailwindcss/plugin')
module.exports = plugin.withOptions(
function (options = {}) {
const prefix = options.prefix || '@'
return function ({ addVariant }) {
addVariant(`${prefix}sm`, `@container (min-width: 320px)`)
addVariant(`${prefix}md`, `@container (min-width: 640px)`)
addVariant(`${prefix}lg`, `@container (min-width: 1024px)`)
}
},
function (options = {}) {
return {
theme: {
extend: {
// Theme extensions
},
},
}
}
)
// tailwind.config.js
plugins: [
require('./plugins/container-queries')({ prefix: 'cq-' }),
]
Official Plugins Worth Knowing
module.exports = {
plugins: [
require('@tailwindcss/typography'), // Prose styling for rendered markdown/HTML
require('@tailwindcss/forms'), // Better default form styling
require('@tailwindcss/aspect-ratio'),// Aspect ratio utilities (pre-native support)
require('@tailwindcss/container-queries'), // Container query support
],
}
Best Practices
- Use
theme()to reference config values. Never hardcode colors or spacing in plugin code; always pull from the theme so users can customize. - Prefer
matchUtilitiesfor value-accepting utilities. It gives users arbitrary value support ([...]) for free and integrates with the theme. - Separate plugins into files when they grow beyond a few lines or when they need to be shared across projects.
- Use
addComponentsfor multi-property classes andaddUtilitiesfor single-property classes. This affects how they interact with@applyand specificity. - Document your plugin's classes. Custom plugins are invisible to IDE tooling unless you also provide a configuration for the Tailwind CSS IntelliSense extension.
- Test with PurgeCSS. Ensure your plugin-generated classes are included in the final output by verifying content scanning picks them up.
Common Pitfalls
- Forgetting to wrap the plugin with
require('tailwindcss/plugin'). Raw functions do not work; you must use theplugin()wrapper. - Using CSS property names instead of camelCase. Inside plugin JavaScript, use
backgroundColornotbackground-color. Tailwind converts them to CSS. - Not handling the
e()escape function for dynamic class names. If you generate class names from user input (e.g., color names with slashes), usee()to properly escape them. - Overusing plugins for things
@applyhandles. If you only need a shorthand for a few utilities,@applyin a CSS file is simpler than a plugin. - Ignoring the distinction between utilities and components. Utilities are meant to be single-purpose and composable. Components can be more opinionated. Putting complex multi-property styles in
addUtilitiesviolates the utility mental model.
Anti-Patterns
-
Plugin for everything. Writing a plugin to create
.text-brandwhenextend: { colors: { brand: '#...' } }in the config achieves the same result with zero code. Plugins should be reserved for capabilities that configuration alone cannot provide. -
Hardcoded values inside plugin code. Using raw hex colors, pixel values, or font names instead of
theme('colors.primary')ortheme('spacing.4')makes the plugin impossible to customize through the config and defeats Tailwind's design-system approach. -
Anonymous inline plugins in config. Embedding plugin logic directly in the
pluginsarray oftailwind.config.jsas an unnamed function makes it invisible to code search, untestable, and non-portable. Extract plugins into named files. -
Recreating official plugins. Building custom typography prose styles, form resets, or container query support when
@tailwindcss/typography,@tailwindcss/forms, and@tailwindcss/container-queriesalready exist and are actively maintained wastes time and introduces bugs. -
Forgetting the
e()escape function. When generating class names dynamically from user input or theme keys that contain special characters (slashes, dots, brackets), unescaped class names produce invalid CSS selectors that silently fail.
Install this skill directly: skilldb add tailwind-skills
Related Skills
Tailwind Animations
Animation utilities, transitions, custom keyframes, and motion patterns with Tailwind CSS
Tailwind Component Patterns
Common UI component patterns including cards, navbars, forms, modals, and badges built with Tailwind CSS
Tailwind Custom Config
Customizing tailwind.config.js to extend themes, add custom colors, fonts, spacing, and configure content paths
Tailwind Dark Mode
Dark mode strategies including class-based toggling, media queries, and CSS variable theming with Tailwind CSS
Tailwind Fundamentals
Utility-first CSS fundamentals with Tailwind including class composition, spacing, typography, and layout primitives
Tailwind Responsive Design
Responsive breakpoints, mobile-first design patterns, and adaptive layouts with Tailwind CSS