matchUtilities, addVariant and the @plugin directive
Recurring Magento specific patterns such as loading states, store grid spacing or Alpine visibility states cannot always be mapped cleanly onto standard utilities. A custom Tailwind plugin captures this knowledge once and makes it reusable across the entire Hyvä theme, including correct compilation in the build pipeline.
Table of Contents
- 1. When a custom Tailwind plugin for Hyvä pays off
- 2. Basic structure of a Tailwind plugin
- 3. matchUtilities for dynamic Magento values
- 4. addComponents for recurring Hyvä patterns
- 5. addVariant for Alpine.js visibility states
- 6. The @plugin directive in Tailwind v4
- 7. Compiling the plugin and wiring it into the build pipeline
- 8. Common mistakes with custom Tailwind plugins
- 9. Plugin API approaches compared
- 10. Summary
- 11. FAQ
1. When a custom Tailwind plugin for Hyvä pays off
Not every recurring pattern in a Hyvä theme should end up as a component class in a CSS file. Once a pattern depends on dynamic values, for example the number of store grid columns, or needs to be reused across projects, a real Tailwind plugin is the more robust solution. A Tailwind plugin is essentially a JavaScript function that gets access to Tailwind's internal utility generation and can therefore register new utilities, components or variants programmatically.
For Hyvä projects, a Tailwind plugin pays off especially when the same logic is needed across multiple tenant themes, for example Mironsoft and the parallel Abrams variant. Instead of redefining the same utility combinations in every theme, the plugin is written once, versioned, and included in both tailwind.config.js files. The following sections show the relevant plugin APIs and how to compile them correctly.
2. Basic structure of a Tailwind plugin
A Tailwind plugin is created through the plugin() function from the tailwindcss/plugin package. The function receives an object with helper functions such as addUtilities, addComponents, addVariant and matchUtilities, through which new CSS gets registered. Unlike manually written CSS, a Tailwind plugin automatically respects the configured prefix setting, the dark mode strategy, and the ordering of generated rules in the final bundle.
The basic structure stays stable across projects: a dedicated file in the theme's plugins directory, a clearly named export, and a registration in the Tailwind configuration. This structure makes the Tailwind plugin testable, since it can be run in isolation with a simple Node script, without triggering the entire Magento build process.
// app/design/frontend/Mironsoft/default/web/tailwind/plugins/hyva-magento-utilities.js
const plugin = require('tailwindcss/plugin');
module.exports = plugin(function ({ addUtilities, addComponents, addVariant, matchUtilities, theme }) {
// Utilities, components and variants are registered here.
// See the following sections for concrete examples.
});
3. matchUtilities for dynamic Magento values
matchUtilities generates utility classes with a variable value part, similar to Tailwind's built in arbitrary values, but with its own validation and its own namespace. For Hyvä projects this is excellent for exposing Magento specific grid column widths as a dedicated Tailwind plugin utility, for example store-cols-4 for a four column product list, whose exact width is derived from the Magento grid configuration.
The advantage over pure arbitrary values like grid-cols-[repeat(4,1fr)] is central control: if the Magento grid definition changes, only the Tailwind plugin needs adjusting, not every single place in the templates where the class is used. That significantly reduces inconsistencies between the category page, search results page and related products.
const plugin = require('tailwindcss/plugin');
module.exports = plugin(function ({ matchUtilities, theme }) {
const storeGridColumns = {
2: 'repeat(2, minmax(0, 1fr))',
3: 'repeat(3, minmax(0, 1fr))',
4: 'repeat(4, minmax(0, 1fr))',
6: 'repeat(6, minmax(0, 1fr))'
};
matchUtilities(
{
'store-cols': (value) => ({
gridTemplateColumns: value
})
},
{ values: storeGridColumns }
);
});
4. addComponents for recurring Hyvä patterns
addComponents registers complete class combinations as standalone components, similar to @layer components in CSS, but programmatically and therefore easier to share across multiple themes. For a Hyvä Tailwind plugin this is a good fit for shipping recurring card patterns for product tiles, CMS blocks or checkout summaries as a package that works with the same class names in every project.
The difference from a plain CSS file using @layer components is the ability to access theme() values and thereby bake design tokens directly into the generated components, without having to reference CSS custom properties manually. This makes the Tailwind plugin more robust against token changes, since the values are resolved at build time.
const plugin = require('tailwindcss/plugin');
module.exports = plugin(function ({ addComponents, theme }) {
addComponents({
'.hyva-product-card': {
borderRadius: theme('borderRadius.2xl'),
border: `1px solid ${theme('colors.slate.200')}`,
padding: theme('spacing.4'),
backgroundColor: theme('colors.white'),
transition: 'box-shadow 150ms ease',
'&:hover': {
boxShadow: theme('boxShadow.lg')
}
}
});
});
5. addVariant for Alpine.js visibility states
addVariant registers new selector prefixes, similar to hover: or focus:, but for project specific states. For Hyvä themes a classic use case is the Alpine.js directive x-cloak, which hides elements before Alpine has initialized. A custom Tailwind plugin can register a cloaked: variant that reacts exactly to this state, instead of repeating the same selector manually in every template.
This pattern extends to other Alpine specific states as well, for example an htmx-loading: prefix for elements during an in flight network request. Defining it centrally in the Tailwind plugin ensures every developer on the team uses the same selector, instead of inventing different, semantically equivalent but syntactically diverging CSS selectors.
const plugin = require('tailwindcss/plugin');
module.exports = plugin(function ({ addVariant }) {
// cloaked:hidden — matches Alpine.js [x-cloak] state before initialization
addVariant('cloaked', '&[x-cloak]');
// htmx-loading:opacity-50 — matches elements during an in-flight request
addVariant('htmx-loading', '&.htmx-request');
});
6. The @plugin directive in Tailwind v4
Tailwind CSS v4 relies on a CSS first configuration approach, in which the classic tailwind.config.js becomes optional. Custom JavaScript plugins remain fully compatible, but are loaded directly in the CSS entry file through the new @plugin directive, instead of being registered in a separate JS configuration object. For a Hyvä Tailwind plugin this means a shorter, more transparent chain from source code to the final CSS bundle.
It matters that the path in the @plugin directive is resolved relative to the CSS file, not relative to the project root. In nested Hyvä theme structures with multiple subfolders, an incorrect relative path leads to a silent failure where the Tailwind plugin simply does not load, without the build visibly breaking.
/* app/design/frontend/Mironsoft/default/web/tailwind/tailwind-source.css */
@import "tailwindcss";
/* Load custom plugins directly in the CSS-first configuration */
@plugin "./plugins/hyva-magento-utilities.js";
@plugin "./plugins/hyva-alpine-variants.js";
@theme {
--color-primary: #0369a1;
}
7. Compiling the plugin and wiring it into the build pipeline
A Tailwind plugin itself does not need to be transpiled separately, as long as it uses plain, node compatible JavaScript without modern syntax features the Tailwind CLI process cannot understand. For the Mironsoft build workflow it is enough to place the plugin as a CommonJS module in the plugins directory and include it in the regular build via bin/npm --prefix app/design/frontend/Vendor/theme/web/tailwind run build.
For projects with multiple themes, for example the dual vendor structure between Mironsoft and Abrams, it is worth maintaining the Tailwind plugin as a small, versioned npm package in the internal registry. Both theme configurations then reference the same package version, and a bug fix in the plugin only needs to be published once and updated in both projects, instead of being maintained as two parallel copies.
8. Common mistakes with custom Tailwind plugins
The most common mistake is using fixed pixel values instead of theme() references inside the Tailwind plugin. That decouples the plugin from future design token changes and forces manual adjustments in multiple places once, for example, the theme's base spacing changes. A second mistake is missing fallback values in matchUtilities, which leads to unclear error messages when a developer uses an undefined variant of the utility.
A third, subtler mistake concerns CSP compliance: some naive Tailwind plugin implementations dynamically add styles at runtime through JavaScript instead of resolving everything to static CSS at build time. For the Hyvä CSP theme this is a problem, because any runtime style injection without a nonce violates the content security policy. A correct Tailwind plugin generates exclusively static CSS at build time.
9. Plugin API approaches compared
Tailwind offers several plugin APIs for different purposes. The table below ranks them by use case for a Hyvä Tailwind plugin.
| API | Purpose | Hyvä use case | Dynamic values? |
|---|---|---|---|
addUtilities |
Fixed, simple utility classes | Hiding scrollbars, text truncate variants | No |
matchUtilities |
Utilities with a variable value | Store grid columns, dynamic spacing | Yes |
addComponents |
Ready made class combinations | Product cards, CMS buttons, cross project | Partially via theme() |
addVariant |
New selector prefixes | x-cloak, htmx-request, project specific states | No |
Mironsoft
Hyvä theme development and Tailwind build tooling
Recurring patterns as your own Tailwind plugin?
We build tailor made Tailwind plugins for your Hyvä themes, CSP compliant, versioned, and reusable across multiple tenant themes.
Plugin development
matchUtilities, addComponents and addVariant for your Magento patterns
v4 migration
Moving existing plugins to the @plugin directive and CSS-first config
CSP review
Ensuring plugins generate exclusively static build time CSS
10. Summary
A custom Tailwind plugin pays off once a Hyvä pattern needs dynamic values or needs to be reused across multiple tenant themes. matchUtilities covers dynamic Magento values such as store grid columns, addComponents bundles recurring card patterns with access to design tokens, and addVariant registers project specific selectors like cloaked: for Alpine.js states.
In Tailwind v4, these plugins are loaded directly in the CSS entry file through the @plugin directive, which shortens the configuration chain. What matters most is that every Tailwind plugin generates exclusively static CSS at build time, never runtime styles, to stay compatible with the Hyvä CSP theme's strict content security policy.
Custom Tailwind Plugins for Hyvä — The Essentials at a Glance
matchUtilities
For dynamic values like Magento grid columns, maintained centrally instead of scattered arbitrary values.
addComponents
Ready made patterns with access to theme() values, shareable across Mironsoft and Abrams.
addVariant
New selector prefixes like cloaked: for Alpine.js x-cloak, defined centrally instead of repeated per template.
@plugin directive
Tailwind v4 loads JS plugins directly from the CSS file, mind relative paths to the CSS file.