Skip to main content
Technology & EngineeringTailwind355 lines

Tailwind Plugins

Writing custom Tailwind CSS plugins to add utilities, components, base styles, and variants

Quick Summary25 lines
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 lines
Paste into your CLAUDE.md or agent config

Writing 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:

FunctionPurpose
addUtilitiesRegister new utility classes
addComponentsRegister component classes
addBaseAdd base/reset styles
addVariantCreate custom variants
matchUtilitiesCreate dynamic utilities that accept values from the theme
themeAccess resolved theme values
eEscape 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 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.

Common Pitfalls

  • 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.
  • 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 addUtilities violates the utility mental model.

Anti-Patterns

  • Plugin for everything. Writing a plugin to create .text-brand when extend: { 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') or theme('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 plugins array of tailwind.config.js as 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-queries already 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

Get CLI access →