| |

Tailwind CSS 67 🎨 Custom JavaScript Plugin Development with Plugin API Factories

Tailwind’s plugin system is the extension point that turns the framework from a fixed utility set into a customizable engine. A plugin is a JavaScript function that receives the Tailwind API and registers new utilities, components, variants, or base styles. The plugin API is the same API that the core uses, which means a custom plugin has the same power as the built-in utilities. The factories are the higher-level abstractions that produce the plugin functions, and they are the mechanism for the reusable, configurable plugins that teams share.

Key point: A plugin is a function that receives the { addUtilities, addComponents, addVariant, addBase, theme, matchUtilities, ... } API. The function runs once at build time, and the registrations are collected and emitted into the final CSS. The plugin does not run at runtime.


Why the plugin system exists

The fixed-utility problem. The core utilities cover the common cases. The design system has the specific cases: the brand colors, the custom spacing scale, the component classes. The plugin is the mechanism for adding the specific cases without forking the framework.

The repetition problem. The same set of the custom utilities appears across the projects. The team copies the CSS from the project to the project. The plugin packages the utilities once, and the projects install the package.

The API problem. The plugin API is the same API the core uses. The plugin can read the theme, generate the utilities, and register the variants. The power is the same as the framework’s own.

The composition problem. The plugins compose. A project installs the typography plugin, the forms plugin, and the custom brand plugin. The three register their utilities, and the final CSS contains all of them.

The distribution problem. A plugin is a package. The team publishes the package to the npm registry, and the projects install it. The versioning and the changelog are the package’s.

The factory problem. A plugin that takes the options is a factory. The factory returns the plugin function, and the options configure the registrations. The plugin(handler, config) function is the built-in factory.

The testing problem. The plugin is a function with the clear inputs and the outputs. The test renders the Tailwind build with the plugin, and the assertions check the generated CSS.


a. The plugin function

A plugin is a function that receives the Tailwind API object. The function is passed to the plugin() helper, which is added to the plugins array in the configuration.

The basic plugin

// tailwind.config.js
const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(({ addUtilities }) => {
      addUtilities({
        '.content-auto': {
          'content-visibility': 'auto',
        },
      });
    }),
  ],
};

The plugin() function takes the handler, and the handler receives the API. The addUtilities registers the .content-auto class.

The handler API

The handler receives an object with the registration methods.

MethodPurpose
addUtilitiesRegister the utility classes
addComponentsRegister the component classes
addBaseRegister the base styles
addVariantRegister the custom variant
matchUtilitiesRegister the dynamic utilities
matchComponentsRegister the dynamic components
matchVariantRegister the dynamic variant
themeRead the theme values
configRead the config values
corePluginsCheck the core plugin

The addUtilities

The addUtilities registers the static utility classes.

plugin(({ addUtilities }) => {
  addUtilities({
    '.skew-10deg': {
      transform: 'skewY(-10deg)',
    },
    '.skew-15deg': {
      transform: 'skewY(-15deg)',
    },
  });
}),

Each key is the class name, and the value is the CSS declarations.

The addComponents

The addComponents registers the component classes.

plugin(({ addComponents }) => {
  addComponents({
    '.btn': {
      padding: '0.5rem 1rem',
      borderRadius: '0.375rem',
      fontWeight: '600',
    },
    '.btn-primary': {
      backgroundColor: '#3b82f6',
      color: '#ffffff',
    },
  });
}),

The difference between the utilities and the components is the intent, not the CSS. The components are the reusable UI pieces, and the utilities are the atomic properties.

The addBase

The addBase registers the base styles.

plugin(({ addBase, theme }) => {
  addBase({
    'h1': { fontSize: theme('fontSize.2xl') },
    'h2': { fontSize: theme('fontSize.xl') },
  });
}),

The addBase is the place for the element defaults and the CSS resets.

The addVariant

The addVariant registers the custom variant.

plugin(({ addVariant }) => {
  addVariant('optional', '&:optional');
  addVariant('hocus', ['&:hover', '&:focus']);
  addVariant('supports-grid', '@supports (display: grid)');
}),

The addVariant takes the name and the selector or the list. The optional: and the hocus: and the supports-grid: are the variants.

The theme function

The theme function reads the theme values.

plugin(({ addUtilities, theme }) => {
  const spacing = theme('spacing');
  addUtilities({
    '.stack > * + *': {
      marginTop: theme('spacing.4'),
    },
  });
}),

The theme('spacing.4') reads the value from the theme, and the plugin uses it.

The matchUtilities

The matchUtilities registers the dynamic utilities.

plugin(({ matchUtilities, theme }) => {
  matchUtilities(
    {
      'text-shadow': (value) => ({
        textShadow: value,
      }),
    },
    { values: theme('textShadow') }
  );
}),

The matchUtilities generates the text-shadow-* classes from the theme values. The text-shadow-sm and the text-shadow-md are the examples.

The plugin function table

APIPurpose
plugin(handler, config)Create the plugin
addUtilitiesThe static utilities
addComponentsThe component classes
addBaseThe base styles
addVariantThe custom variant
matchUtilitiesThe dynamic utilities
themeThe theme values

b. The factory pattern

The factory is a function that takes the options and returns the plugin. The plugin() helper is the built-in factory, and the custom factory is the pattern for the configurable plugins.

The factory function

// my-plugin.js
const plugin = require('tailwindcss/plugin');

module.exports = function myPlugin(options = {}) {
  const {
    prefix = 'my',
    colors = {},
  } = options;

  return plugin(({ addUtilities, addVariant }) => {
    addUtilities({
      [`.${prefix}-content-auto`]: {
        'content-visibility': 'auto',
      },
    });

    for (const [name, value] of Object.entries(colors)) {
      addUtilities({
        [`.${prefix}-text-${name}`]: { color: value },
        [`.${prefix}-bg-${name}`]: { backgroundColor: value },
      });
    }

    addVariant(`${prefix}-optional`, '&:optional');
  });
};

The factory takes the options and returns the plugin() call. The options configure the prefix and the colors.

The usage

// tailwind.config.js
module.exports = {
  plugins: [
    require('./my-plugin')({
      prefix: 'brand',
      colors: {
        primary: '#3b82f6',
        accent: '#8b5cf6',
      },
    }),
  ],
};

The factory is called with the options, and the returned plugin is added to the array.

The plugin with the config

The plugin() helper accepts a second argument, the config.

plugin(
  ({ addUtilities, theme }) => {
    const values = theme('myPlugin');
    addUtilities({
      '.my-util': {
        padding: values.spacing,
      },
    });
  },
  {
    theme: {
      myPlugin: {
        spacing: '1rem',
      },
    },
  }
);

The second argument extends the theme with the plugin’s own namespace. The handler reads the values with the theme('myPlugin').

The default options

The factory merges the user options with the defaults.

const defaults = {
  prefix: 'my',
  spacing: '1rem',
};

module.exports = function myPlugin(options = {}) {
  const config = { ...defaults, ...options };
  return plugin(({ addUtilities }) => {
    addUtilities({
      [`.${config.prefix}-box`]: {
        padding: config.spacing,
      },
    });
  });
};

The spread merges the defaults and the user options.

The validation

The factory validates the options and throws the helpful error.

module.exports = function myPlugin(options = {}) {
  if (options.prefix && typeof options.prefix !== 'string') {
    throw new Error('The prefix option must be a string');
  }
  // ...
};

The validation catches the misconfiguration at the build time.

The factory table

PatternPurpose
Factory functionThe options to the plugin
plugin(handler, config)The theme namespace
The defaults mergeThe fallback options
The validationThe helpful error

c. The real plugin patterns

The patterns below are the ones that appear in the published plugins.

The text-shadow plugin

const plugin = require('tailwindcss/plugin');

module.exports = plugin(({ matchUtilities, theme }) => {
  matchUtilities(
    {
      'text-shadow': (value) => ({
        textShadow: value,
      }),
    },
    { values: theme('textShadow') }
  );
}, {
  theme: {
    textShadow: {
      sm: '0 1px 2px rgb(0 0 0 / 0.1)',
      DEFAULT: '0 2px 4px rgb(0 0 0 / 0.1)',
      lg: '0 8px 16px rgb(0 0 0 / 0.1)',
    },
  },
});

The matchUtilities generates the text-shadow-sm and the text-shadow-lg from the theme values.

The animation-delay plugin

module.exports = plugin(({ matchUtilities, theme }) => {
  matchUtilities(
    {
      'animation-delay': (value) => ({
        animationDelay: value,
      }),
    },
    { values: theme('animationDelay') }
  );
}, {
  theme: {
    animationDelay: {
      75: '75ms',
      100: '100ms',
      150: '150ms',
      200: '200ms',
    },
  },
});

The animation-delay-100 and the animation-delay-200 are the examples.

The fluid-type plugin

module.exports = function fluidType({ min = 16, max = 24 } = {}) {
  return plugin(({ matchUtilities, theme }) => {
    matchUtilities(
      {
        'fluid-text': (value) => {
          const [minVw, maxVw] = value.split('-').map(Number);
          const slope = (max - min) / (maxVw - minVw);
          const intercept = min - slope * minVw;
          return {
            fontSize: `clamp(${min / 16}rem, ${intercept / 16}rem + ${slope * 100}vw, ${max / 16}rem)`,
          };
        },
      },
      { values: theme('fluidText') }
    );
  });
};

The factory takes the min and the max, and the matchUtilities generates the fluid-text-* classes.

The container-query plugin

module.exports = plugin(({ addVariant, matchVariant }) => {
  addVariant('@sm', '@container (min-width: 24rem)');
  addVariant('@md', '@container (min-width: 28rem)');
  addVariant('@lg', '@container (min-width: 32rem)');

  matchVariant(
    '@',
    (value) => `@container (min-width: ${value})`,
    {
      values: {
        sm: '24rem',
        md: '28rem',
        lg: '32rem',
      },
    }
  );
}, {
  theme: {
    containers: {
      sm: '24rem',
      md: '28rem',
      lg: '32rem',
    },
  },
});

The addVariant registers the fixed variants, and the matchVariant registers the dynamic ones.

The icon plugin

module.exports = function icons({ prefix = 'icon' } = {}) {
  return plugin(({ addUtilities, theme }) => {
    const icons = theme('icons', {});
    const utilities = {};

    for (const [name, path] of Object.entries(icons)) {
      utilities[`.${prefix}-${name}`] = {
        backgroundImage: `url("${path}")`,
        backgroundSize: 'contain',
        backgroundRepeat: 'no-repeat',
        backgroundPosition: 'center',
        display: 'inline-block',
        width: '1em',
        height: '1em',
      };
    }

    addUtilities(utilities);
  });
};

The factory generates the icon classes from the theme values.

The pattern table

PluginAPI
Text shadowmatchUtilities
Animation delaymatchUtilities
Fluid typematchUtilities + the factory
Container queryaddVariant + matchVariant
IconaddUtilities + the theme

Complete Example Session

// ============================================
// PART 1: THE BASIC PLUGIN
// ============================================

const plugin = require('tailwindcss/plugin');

module.exports = {
  plugins: [
    plugin(({ addUtilities }) => {
      addUtilities({
        '.content-auto': {
          'content-visibility': 'auto',
        },
      });
    }),
  ],
};

// ============================================
// PART 2: THE ADD-COMPONENTS
// ============================================

plugin(({ addComponents }) => {
  addComponents({
    '.btn': {
      padding: '0.5rem 1rem',
      borderRadius: '0.375rem',
    },
    '.btn-primary': {
      backgroundColor: '#3b82f6',
      color: '#ffffff',
    },
  });
}),

// ============================================
// PART 3: THE ADD-VARIANT
// ============================================

plugin(({ addVariant }) => {
  addVariant('optional', '&:optional');
  addVariant('hocus', ['&:hover', '&:focus']);
}),

// ============================================
// PART 4: THE MATCH-UTILITIES
// ============================================

plugin(({ matchUtilities, theme }) => {
  matchUtilities(
    {
      'text-shadow': (value) => ({
        textShadow: value,
      }),
    },
    { values: theme('textShadow') }
  );
}, {
  theme: {
    textShadow: {
      sm: '0 1px 2px rgb(0 0 0 / 0.1)',
      lg: '0 8px 16px rgb(0 0 0 / 0.1)',
    },
  },
}),

// ============================================
// PART 5: THE THEME FUNCTION
// ============================================

plugin(({ addUtilities, theme }) => {
  addUtilities({
    '.stack > * + *': {
      marginTop: theme('spacing.4'),
    },
  });
}),

// ============================================
// PART 6: THE FACTORY
// ============================================

// my-plugin.js
module.exports = function myPlugin(options = {}) {
  const { prefix = 'my', colors = {} } = options;

  return plugin(({ addUtilities, addVariant }) => {
    addUtilities({
      [`.${prefix}-content-auto`]: {
        'content-visibility': 'auto',
      },
    });

    for (const [name, value] of Object.entries(colors)) {
      addUtilities({
        [`.${prefix}-text-${name}`]: { color: value },
      });
    }

    addVariant(`${prefix}-optional`, '&:optional');
  });
};

// ============================================
// PART 7: THE FACTORY USAGE
// ============================================

// tailwind.config.js
module.exports = {
  plugins: [
    require('./my-plugin')({
      prefix: 'brand',
      colors: {
        primary: '#3b82f6',
        accent: '#8b5cf6',
      },
    }),
  ],
};

// ============================================
// PART 8: THE PLUGIN WITH THE CONFIG
// ============================================

plugin(
  ({ addUtilities, theme }) => {
    const values = theme('myPlugin');
    addUtilities({
      '.my-util': {
        padding: values.spacing,
      },
    });
  },
  {
    theme: {
      myPlugin: {
        spacing: '1rem',
      },
    },
  }
),

// ============================================
// PART 9: THE TEXT-SHADOW PLUGIN
// ============================================

module.exports = plugin(({ matchUtilities, theme }) => {
  matchUtilities(
    {
      'text-shadow': (value) => ({ textShadow: value }),
    },
    { values: theme('textShadow') }
  );
}, {
  theme: {
    textShadow: {
      sm: '0 1px 2px rgb(0 0 0 / 0.1)',
      lg: '0 8px 16px rgb(0 0 0 / 0.1)',
    },
  },
});

// ============================================
// PART 10: THE COMPLETE PATTERN
// ============================================

// 1. Use the plugin() helper to create the plugin.
// 2. Use the addUtilities for the static classes.
// 3. Use the addComponents for the component classes.
// 4. Use the addBase for the element defaults.
// 5. Use the addVariant for the custom variants.
// 6. Use the matchUtilities for the dynamic utilities.
// 7. Use the theme() to read the theme values.
// 8. Use the factory pattern for the configurable plugins.
// 9. Use the second argument for the theme namespace.
// 10. Publish the plugin as the npm package.

The ten parts showed the basic plugin, the addComponents, the addVariant, the matchUtilities, the theme function, the factory, the factory usage, the plugin with the config, the text-shadow plugin, and the complete pattern.


Quick Reference

The Plugin API

MethodPurpose
addUtilitiesThe static utilities
addComponentsThe component classes
addBaseThe base styles
addVariantThe custom variant
matchUtilitiesThe dynamic utilities
matchComponentsThe dynamic components
matchVariantThe dynamic variant
themeThe theme values
configThe config values
corePluginsThe core check

The plugin() Helper

ArgumentPurpose
The handlerThe API callback
The configThe theme extension

The Factory Pattern

StepAction
1Define the factory function
2Merge the defaults and the options
3Return the plugin() call
4Use the options in the handler

The addVariant Forms

FormExample
The selector'&:optional'
The list['&:hover', '&:focus']
The at-rule'@supports (display: grid)'
The function(value) => '&:nth-child(...)'

The matchUtilities Options

OptionPurpose
valuesThe theme values
typeThe value type
supportsVariantsThe variant support

Best Practices

✅ Do This:

// Use the plugin() helper.
plugin(({ addUtilities }) => { ... });  // ✅

// Use the addUtilities for the static classes.
addUtilities({ '.content-auto': { 'content-visibility': 'auto' } });  // ✅

// Use the addComponents for the reusable pieces.
addComponents({ '.btn': { ... } });  // ✅

// Use the matchUtilities for the dynamic utilities.
matchUtilities({ 'text-shadow': (v) => ({ textShadow: v }) }, { values: theme('textShadow') });  // ✅

// Use the theme() to read the values.
marginTop: theme('spacing.4');  // ✅

// Use the factory for the configurable plugins.
module.exports = function myPlugin(options = {}) { return plugin(...); };  // ✅

// Use the second argument for the theme namespace.
plugin(handler, { theme: { myPlugin: { ... } } });  // ✅

// Validate the options.
if (typeof options.prefix !== 'string') throw new Error('...');  // ✅

❌ Don’t Do This:

// Don't run the logic at the runtime.
// The plugin runs at the build time.  // ❌

// Don't use the addUtilities for the dynamic values.
addUtilities({ '.text-shadow-lg': { ... } });  // ⚠️ Use the matchUtilities

// Don't forget the theme namespace.
plugin(handler);  // ⚠️ The theme() call fails

// Don't hardcode the values.
'.btn': { backgroundColor: '#3b82f6' };  // ⚠️ Use the theme

// Don't skip the validation.
module.exports = function (options) { /* no validation */ };  // ❌

// Don't mutate the options.
options.prefix = 'new';  // ❌ The caller's object

// Don't duplicate the core utilities.
addUtilities({ '.flex': { display: 'flex' } });  // ❌ The core has it

Common Pitfalls

PitfallWhy It HappensFix
The utility does not generateThe addUtilities syntaxCheck the object form
The dynamic utility missingThe addUtilities usedUse the matchUtilities
The theme value is undefinedNo namespaceAdd the second argument
The variant does not applyThe wrong selectorCheck the addVariant
The class collisionThe prefix missingUse the factory’s prefix
The plugin does not runThe config missingAdd to the plugins array
The options are ignoredThe factory not calledCall the factory with the options

Real-World Examples

1. The Basic Plugin

plugin(({ addUtilities }) => {
  addUtilities({ '.content-auto': { 'content-visibility': 'auto' } });
}),

2. The Component

addComponents({ '.btn': { padding: '0.5rem 1rem' } }),

3. The Variant

addVariant('optional', '&:optional'),

4. The Dynamic Utility

matchUtilities({ 'text-shadow': (v) => ({ textShadow: v }) }, { values: theme('textShadow') }),

5. The Theme

marginTop: theme('spacing.4'),

6. The Factory

module.exports = function (options = {}) { return plugin(({ ... }) => { ... }); };

7. The Config

plugin(handler, { theme: { myPlugin: { spacing: '1rem' } } }),

8. The Text Shadow

matchUtilities({ 'text-shadow': (v) => ({ textShadow: v }) }, { values: theme('textShadow') }),

9. The Animation Delay

matchUtilities({ 'animation-delay': (v) => ({ animationDelay: v }) }, { values: theme('animationDelay') }),

10. The Complete Pattern

module.exports = require('./my-plugin')({ prefix: 'brand' });

Visual

The Plugin Lifecycle

┌────────────────────────────────────────────────────────────┐
│  THE PLUGIN LIFECYCLE                                      │
│                                                            │
│  BUILD TIME                                                │
│  ┌──────────────────────────────────────────────────┐     │
│  │  1. The config is read.                           │     │
│  │  2. The plugin function is called.                │     │
│  │  3. The handler receives the API.                 │     │
│  │  4. The registrations are collected.              │     │
│  │  5. The CSS is generated.                         │     │
│  └──────────────────────────────────────────────────┘     │
│                                                            │
│  RUNTIME                                                   │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The plugin does not run.                         │     │
│  │  The generated CSS is the output.                 │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Factory

┌────────────────────────────────────────────────────────────┐
│  THE FACTORY                                               │
│                                                            │
│  module.exports = function myPlugin(options = {}) {        │
│    const config = { ...defaults, ...options };             │
│    return plugin(({ addUtilities }) => {                   │
│      addUtilities({                                        │
│        [`.${config.prefix}-box`]: { ... },                 │
│      });                                                   │
│    });                                                     │
│  };                                                        │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The factory takes the options.                   │     │
│  │  The factory returns the plugin.                  │     │
│  │  The config is closed over.                       │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The matchUtilities

┌────────────────────────────────────────────────────────────┐
│  THE matchUtilities                                        │
│                                                            │
│  matchUtilities(                                           │
│    {                                                       │
│      'text-shadow': (value) => ({ textShadow: value }),    │
│    },                                                      │
│    { values: theme('textShadow') }                         │
│  );                                                        │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  The theme values:                                │     │
│  │    sm: '0 1px 2px ...'                            │     │
│  │    lg: '0 8px 16px ...'                           │     │
│  │                                                   │     │
│  │  The generated classes:                           │     │
│  │    .text-shadow-sm { text-shadow: 0 1px 2px ... } │     │
│  │    .text-shadow-lg { text-shadow: 0 8px 16px ... }│     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

The Plugin Composition

┌────────────────────────────────────────────────────────────┐
│  THE PLUGIN COMPOSITION                                    │
│                                                            │
│  plugins: [                                                │
│    require('@tailwindcss/typography'),                     │
│    require('@tailwindcss/forms'),                          │
│    require('./my-brand-plugin')({ prefix: 'brand' }),      │
│    require('./my-animation-plugin')({ duration: 300 }),    │
│  ]                                                         │
│                                                            │
│  ┌──────────────────────────────────────────────────┐     │
│  │  Each plugin registers its own utilities.         │     │
│  │  The final CSS contains all of them.              │     │
│  │  The plugins do not conflict if the names         │     │
│  │  are unique.                                      │     │
│  └──────────────────────────────────────────────────┘     │
└────────────────────────────────────────────────────────────┘

Summary

ItemValue
plugin(handler, config)The plugin helper
addUtilitiesThe static utilities
addComponentsThe component classes
addBaseThe base styles
addVariantThe custom variant
matchUtilitiesThe dynamic utilities
themeThe theme values
FactoryThe options to the plugin
The second argumentThe theme namespace
The build timeThe plugin runs once

Key takeaways:

  • A plugin is a function that receives the Tailwind API. The handler registers the utilities, the components, the base styles, and the variants. The function runs once at the build time, and the registrations are collected into the final CSS.
  • The addUtilities and the addComponents register the static classes. The difference is the intent: the utilities are the atomic properties, and the components are the reusable UI pieces.
  • The addVariant registers the custom variants. The selector, the list, and the at-rule are the forms. The optional: and the hocus: are the examples.
  • The matchUtilities generates the dynamic utilities from the theme values. The text-shadow-sm and the animation-delay-100 are the examples. The theme values are the input, and the classes are the output.
  • The factory pattern is the configurable plugin. The function takes the options and returns the plugin() call. The options configure the prefix, the colors, and the other values.
  • The second argument to the plugin() extends the theme. The plugin’s own namespace holds the values, and the handler reads them with the theme('myPlugin') call.
  • The plugin runs at the build time, not the runtime. The output is the CSS, and the runtime is the browser. The plugin does not affect the bundle’s JavaScript.
  • The plugins compose. The project installs the typography, the forms, and the custom plugins. The final CSS contains all of them, and the names are the only conflict point.

Remember: The Tailwind plugin system is the extension point that adds the custom utilities, the components, the variants, and the base styles. The plugin is a function that receives the API, and the factory is the function that takes the options and returns the plugin. The addUtilities, the addComponents, the addVariant, and the matchUtilities are the registration methods, and the theme() reads the values. The result is a framework that adapts to the design system, and a set of the shared plugins that the teams install.



Stop using slow, ad-bloated tool sites! 🤮

🔎 Search “KandZ Tools” on Google to use many professional utilities for free.

KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)

⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free

🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!