Tailwind CSS Plugins and Presets
Learn to write custom Tailwind plugins, use presets for shared configurations, and extend Tailwind's utility system with your own design primitives.
What you'll learn
- ✓How Tailwind plugins work and what APIs they expose
- ✓How to write a custom utility plugin from scratch
- ✓How to add custom variants for new interactive states
- ✓How to create component plugins with base styles
- ✓How to package shared configurations as presets
Prerequisites
- •Solid understanding of Tailwind CSS utilities and config
- •Comfortable with JavaScript/Node.js
Tailwind’s plugin system is the mechanism behind everything: the official typography plugin, the forms plugin, and even Tailwind’s own core utilities are plugins internally. Writing your own is not hard once you understand the four plugin APIs. This article walks through each one and shows how to package reusable configurations as presets.
Plugin Architecture
A Tailwind plugin is a function that receives a helper object and uses it to register CSS with Tailwind’s build system. There are four main helpers:
addUtilities() -> Register new utility classes (like bg-*, text-*)
addComponents() -> Register component classes (like .btn, .card)
addBase() -> Inject base/reset styles (like normalize)
addVariant() -> Create new variants (like hover:, focus:, but custom)
matchUtilities() -> Register utilities with dynamic values (like bg-[#hex]) Writing Your First Plugin
Let’s create a plugin that adds text-shadow utilities.
// tailwind.config.js
const plugin = require('tailwindcss/plugin');
export default {
plugins: [
plugin(function ({ addUtilities }) {
addUtilities({
'.text-shadow-sm': {
'text-shadow': '0 1px 2px rgb(0 0 0 / 0.1)',
},
'.text-shadow': {
'text-shadow': '0 2px 4px rgb(0 0 0 / 0.15)',
},
'.text-shadow-lg': {
'text-shadow': '0 4px 8px rgb(0 0 0 / 0.2)',
},
'.text-shadow-none': {
'text-shadow': 'none',
},
});
}),
],
};
Now you can use these in your markup:
<h1 class="text-4xl font-bold text-shadow-lg">Hero Title</h1>
<p class="text-shadow-sm">Subtle shadow on paragraph text</p>
These utilities automatically work with Tailwind’s responsive and state prefixes:
<h1 class="text-shadow-sm md:text-shadow-lg hover:text-shadow-none">
Responsive and interactive shadows
</h1>
Dynamic Utilities with matchUtilities
matchUtilities lets you create utilities that accept arbitrary values, just like bg-[#ff0] or p-[17px]:
const plugin = require('tailwindcss/plugin');
export default {
theme: {
extend: {
textShadow: {
sm: '0 1px 2px rgb(0 0 0 / 0.1)',
DEFAULT: '0 2px 4px rgb(0 0 0 / 0.15)',
lg: '0 4px 8px rgb(0 0 0 / 0.2)',
},
},
},
plugins: [
plugin(function ({ matchUtilities, theme }) {
matchUtilities(
{
'text-shadow': (value) => ({
'text-shadow': value,
}),
},
{ values: theme('textShadow') }
);
}),
],
};
Now these all work:
<!-- Named values from config -->
<p class="text-shadow-sm">Small shadow</p>
<p class="text-shadow-lg">Large shadow</p>
<!-- Arbitrary values -->
<p class="text-shadow-[0_4px_12px_rgba(0,0,0,0.3)]">Custom shadow</p>
Adding Component Classes
Component classes sit in the @layer components layer, so utility classes can override them. Use addComponents for reusable UI patterns:
const plugin = require('tailwindcss/plugin');
export default {
plugins: [
plugin(function ({ addComponents, theme }) {
addComponents({
'.btn': {
display: 'inline-flex',
alignItems: 'center',
justifyContent: 'center',
paddingLeft: theme('spacing.4'),
paddingRight: theme('spacing.4'),
paddingTop: theme('spacing.2'),
paddingBottom: theme('spacing.2'),
fontSize: theme('fontSize.sm'),
fontWeight: theme('fontWeight.semibold'),
borderRadius: theme('borderRadius.md'),
transitionProperty: 'background-color, border-color, color',
transitionDuration: '150ms',
'&:focus': {
outline: 'none',
boxShadow: `0 0 0 2px ${theme('colors.white')}, 0 0 0 4px ${theme('colors.blue.500')}`,
},
},
'.btn-primary': {
backgroundColor: theme('colors.blue.600'),
color: theme('colors.white'),
'&:hover': {
backgroundColor: theme('colors.blue.700'),
},
},
'.btn-secondary': {
backgroundColor: theme('colors.gray.100'),
color: theme('colors.gray.800'),
'&:hover': {
backgroundColor: theme('colors.gray.200'),
},
},
});
}),
],
};
Usage:
<button class="btn btn-primary">Save changes</button>
<button class="btn btn-secondary">Cancel</button>
<!-- Utility classes can override component styles -->
<button class="btn btn-primary rounded-full px-8">Custom shape</button>
Adding Base Styles
Base styles apply globally, like a CSS reset. Use addBase for typography defaults, link styles, or custom resets:
plugin(function ({ addBase, theme }) {
addBase({
'h1': {
fontSize: theme('fontSize.3xl'),
fontWeight: theme('fontWeight.bold'),
lineHeight: theme('lineHeight.tight'),
},
'h2': {
fontSize: theme('fontSize.2xl'),
fontWeight: theme('fontWeight.semibold'),
lineHeight: theme('lineHeight.tight'),
},
'a': {
color: theme('colors.blue.600'),
textDecoration: 'underline',
'&:hover': {
color: theme('colors.blue.800'),
},
},
});
}),
Custom Variants
Variants are the prefixes like hover:, focus:, dark:. You can create your own:
plugin(function ({ addVariant }) {
// Matches when a parent has the .sidebar-open class
addVariant('sidebar-open', '.sidebar-open &');
// Matches the element itself when it has a data attribute
addVariant('data-active', '&[data-active="true"]');
// Group variant for specific states
addVariant('group-checked', ':merge(.group):checked &');
// Media query variant
addVariant('supports-grid', '@supports (display: grid)');
}),
Usage:
<!-- Hide element unless sidebar is open -->
<div class="hidden sidebar-open:block">
Sidebar content
</div>
<!-- Style based on data attribute -->
<button class="bg-gray-200 data-active:bg-blue-600 data-active:text-white"
data-active="true">
Active Tab
</button>
<!-- Feature detection -->
<div class="flex supports-grid:grid supports-grid:grid-cols-3">
Graceful degradation
</div>
Extracting Plugins to Separate Files
For larger plugins, move them to their own file:
// plugins/text-shadow.js
const plugin = require('tailwindcss/plugin');
module.exports = plugin(
function ({ matchUtilities, theme }) {
matchUtilities(
{
'text-shadow': (value) => ({
'text-shadow': value,
}),
},
{ values: theme('textShadow') }
);
},
{
// Default theme values shipped with the plugin
theme: {
textShadow: {
sm: '0 1px 2px rgb(0 0 0 / 0.1)',
DEFAULT: '0 2px 4px rgb(0 0 0 / 0.15)',
lg: '0 4px 8px rgb(0 0 0 / 0.2)',
xl: '0 8px 16px rgb(0 0 0 / 0.25)',
},
},
}
);
The second argument to plugin() lets you ship default theme values. Users can override them in their own config:
// tailwind.config.js
export default {
theme: {
extend: {
textShadow: {
subtle: '0 0.5px 1px rgb(0 0 0 / 0.05)',
},
},
},
plugins: [
require('./plugins/text-shadow'),
],
};
Presets: Sharing Full Configurations
A preset is a complete Tailwind config that another project can inherit from. This is how design systems share tokens, plugins, and settings across multiple apps.
// presets/acme-design-system.js
module.exports = {
theme: {
colors: {
transparent: 'transparent',
current: 'currentColor',
white: '#ffffff',
black: '#000000',
brand: {
50: '#eff6ff',
100: '#dbeafe',
500: '#3b82f6',
600: '#2563eb',
700: '#1d4ed8',
900: '#1e3a5f',
},
neutral: {
50: '#fafafa',
100: '#f5f5f5',
200: '#e5e5e5',
500: '#737373',
700: '#404040',
900: '#171717',
},
success: { 500: '#22c55e' },
warning: { 500: '#f59e0b' },
error: { 500: '#ef4444' },
},
fontFamily: {
sans: ['Inter', 'system-ui', 'sans-serif'],
mono: ['JetBrains Mono', 'monospace'],
},
borderRadius: {
none: '0',
sm: '0.25rem',
DEFAULT: '0.5rem',
lg: '0.75rem',
full: '9999px',
},
},
plugins: [
require('@tailwindcss/forms'),
require('@tailwindcss/typography'),
],
};
A consuming project uses it with presets:
// tailwind.config.js (in the app)
export default {
presets: [
require('./presets/acme-design-system'),
],
content: ['./src/**/*.{js,jsx,ts,tsx}'],
theme: {
extend: {
// App-specific overrides layer on top
colors: {
accent: '#8b5cf6',
},
},
},
};
How Presets Merge
Preset theme values + Project theme.extend = Final config
For theme keys:
- theme.extend.* merges (adds to preset values)
- theme.* replaces (overrides preset values entirely)
For plugins:
- Preset plugins + project plugins are combined
For content:
- Project content is used (not inherited from preset) If you set theme.colors in your project (not theme.extend.colors), it completely replaces the preset colors. Use extend to add to a preset’s values.
Publishing a Preset as an npm Package
{
"name": "@acme/tailwind-preset",
"version": "1.0.0",
"main": "preset.js",
"peerDependencies": {
"tailwindcss": ">=3.4"
},
"dependencies": {
"@tailwindcss/forms": "^0.5",
"@tailwindcss/typography": "^0.5"
}
}
Consumers install it and reference it:
presets: [require('@acme/tailwind-preset')],
Official Plugins Worth Knowing
| Plugin | What it does |
|---|---|
@tailwindcss/forms | Resets form elements for clean styling |
@tailwindcss/typography | Adds prose classes for rich text content |
@tailwindcss/aspect-ratio | Aspect ratio utilities (mostly replaced by native CSS) |
@tailwindcss/container-queries | @container and @ prefix utilities |
When to Write a Plugin vs Use @apply
Use a plugin when:
- Multiple projects need the same utilities
- You want integration with the Tailwind theme system
- You need custom variants
- You are building a design system package
Use @apply when:
- You need a one-off component class in a single project
- The pattern is simple and unlikely to change
In most cases, extracting a framework component (a React component, an Astro component) is better than either option. Keep CSS in utilities and keep reusability in components.
Wrap-Up
Tailwind plugins give you four APIs: addUtilities for single-purpose classes, addComponents for multi-property patterns, addBase for global resets, and addVariant for new conditional prefixes. Use matchUtilities for arbitrary value support. Package shared design tokens and plugin sets into presets so multiple projects stay consistent. The plugin system is what makes Tailwind extensible without forking the framework.
Related articles
- Tailwind Tailwind Custom Plugins Tutorial
Write your own Tailwind plugins to add utilities, components, and variants that match your design system without reaching for arbitrary values everywhere.
- Tailwind Design Tokens and Theming with Tailwind CSS
Build a robust theming system using custom design tokens, CSS variables, theme extension, and multi-brand support in Tailwind CSS.
- Tailwind Responsive Design Patterns with Tailwind CSS
Master mobile-first breakpoints, container queries, responsive grids, and real-world layout patterns using Tailwind CSS utilities.
- Tailwind CSS Grid Layouts with Tailwind CSS
Build powerful grid layouts using Tailwind's grid utilities, auto-fill, auto-fit, spanning, subgrid patterns, and real-world layout examples.