Why tailwind-modifier?
Tailwind CSS is a powerful utility-first framework, but managing complex modifiers like hover, focus, group-hover, and responsive breakpoints can become repetitive and error-prone, especially in large-scale projects. Developers often face challenges with:
- Modifier Overhead: Writing and maintaining repeated modifier combinations leads to cluttered and less readable code.
- Nested Modifiers: Expressing deeply nested or conditional states requires verbose and cumbersome syntax.
- Readability & Maintainability: Teams struggle to review and understand heavily modified Tailwind classes, especially when multiple layers of logic are involved.
- Type Safety: Lack of native type-checking makes it harder to catch errors when creating dynamic or custom modifier configurations.
These challenges can slow down development, reduce code clarity, and increase the likelihood of bugs in Tailwind-based projects.
tailwind-modifier aims to solve these problems by introducing a modifier-first approach that simplifies the process, reduces repetition, and enhances readability while providing full type safety.
tailwind-modifier
Tailwind-modifier adopts a modifier-first approach to writing Tailwind classes, simplifying the management and handling of screens, groups, and
other modifiers available in Tailwind CSS.
Features
✓ Complete type-safety
✓ No external dependencies
✓ Full support for all Tailwind CSS modifiers
✓ Nested modifier support - *:dark:bg-dark
✓ Differentiated peers/groups support - group-hover/item
✓ Full compatibility with arbitrary modifiers - group-[.is-published]
✓ Support for arbitrary variant modifiers - [&:nth-child(3)]:underline
✓ Custom modifiers support for type-safety
✓ Works seamlessly with other tools like twMerge
and clsx
✓ Support for tailwind classes auto-completion. See Editor Support
below
Installation
You can install tailwind-modifier
using npm
or your choice of package manager:
npm install tailwind-modifier
Usage
twMod
offers versatile syntax options, providing users with the flexibility to seamlessly integrate the tool into their workflow.
- Pass simple comma separated string with modifiers.
twMod('text-blue-300 hover:font-bold,translateY-[0.5px],text-red-300');
// text-blue-300 hover:font-bold hover:translateY-[0.5px] hover:text-red-300
- Pass multiple string arguments.
twMod
which merge the classes (will not do any conflict resolution if duplicates found).
twMod(
'p-4 rounded text-blue-300',
'hover:font-bold,translateY-[0.5px],text-red-300',
'group-hover:uppercase,border',
);
// p-4 rounded text-blue-300 hover:font-bold hover:translateY-[0.5px] hover:text-red-300 group-hover:uppercase group-hover:border
Note: you can pass the output of this to twMerge
for conflict resolution.
- True power of
twMod
comes with the object syntax which is type-safe and allows for nesting of modifiers.
twMod({
DEFAULT: 'w-3/4 px-16 py-8',
sm: 'w-full px-4 py-4',
md: 'w-5/6 px-8 py-8',
});
// Or
twMod(
'w-3/4 px-16 py-8',
{
// This is still type-safe
sm: 'w-full px-4 py-4',
md: 'w-5/6 px-8 py-8',
},
);
// w-3/4 px-16 py-8 sm:w-full sm:px-4 sm:py-4 md:w-5/6 md:px-8 md:py-8
Note: use the DEFAULT reserved key to set default/root classes
twMod({
DEFAULT: 'text-gray-800 bg-white p-4',
hover: {
DEFAULT: 'bg-gray-100',
dark: {
DEFAULT: 'bg-gray-900 text-white',
focus: 'ring ring-gray-300 outline-none',
},
},
sm: {
DEFAULT: 'p-2 text-gray-700',
hover: {
DEFAULT: 'bg-gray-200',
dark: {
DEFAULT: 'text-gray-300',
active: 'bg-gray-800',
},
},
},
lg: {
DEFAULT: 'p-8 text-gray-900',
'group-hover': {
DEFAULT: 'bg-blue-200',
'peer-hover': {
DEFAULT: 'bg-blue-500',
focus: 'ring ring-blue-300',
},
},
},
});
// or
const defaultScreen: TwModInput = {
DEFAULT: 'text-gray-800 bg-white p-4',
hover: {
DEFAULT: 'bg-gray-100',
dark: {
DEFAULT: 'bg-gray-900 text-white',
focus: 'ring ring-gray-300 outline-none',
},
},
};
const smallScreen: TwModInput = {
sm: {
DEFAULT: 'p-2 text-gray-700',
hover: {
DEFAULT: 'bg-gray-200',
dark: {
DEFAULT: 'text-gray-300',
active: 'bg-gray-800',
},
},
},
};
const largeScreen: TwModInput = {
lg: {
DEFAULT: 'p-8 text-gray-900',
'group-hover': {
DEFAULT: 'bg-blue-200',
'peer-hover': {
DEFAULT: 'bg-blue-500',
focus: 'ring ring-blue-300',
},
},
},
};
twMod(defaultScreen, smallScreen, largeScreen);
// text-gray-800 bg-white p-4 hover:bg-gray-100 hover:dark:bg-gray-900 hover:dark:text-white hover:dark:focus:ring
// hover:dark:focus:ring-gray-300 hover:dark:focus:outline-none sm:p-2 sm:text-gray-700 sm:hover:bg-gray-200
// sm:hover:dark:text-gray-300 sm:hover:dark:active:bg-gray-800 lg:p-8 lg:text-gray-900 lg:group-hover:bg-blue-200
// lg:group-hover:peer-hover:lg:group-hover:peer-hover:focus:ring lg:group-hover:peer-hover:focus:ring-blue-300
- You can use this with
clsx
tool for conditional classes or can directly set conditional classes intwMod
// Conditional classes with clsx
twMod({
DEFAULT: 'w-10 p-4',
sm: ['bg-green-200', clsx({ 'w-20 bg-green-300': open })],
md: clsx({ 'text-white bg-gray-800': isDarkMode }),
lg: {
DEFAULT: 'rounded-lg shadow-lg',
hover: clsx({ 'bg-blue-500 scale-110': isActive }),
'group-focus': clsx({ 'ring-2 ring-green-400': open }),
},
xl: [
'p-8 text-lg',
{
dark: clsx({ 'text-gray-200 bg-gray-900': isDarkMode }),
'group-active': clsx({ 'bg-yellow-300': open }),
},
],
});
// Or without clsx
twMod({
DEFAULT: 'w-10 p-4',
sm: ['bg-green-200', open ? 'w-20 bg-green-300' : ''],
md: isDarkMode && 'text-white bg-gray-800',
lg: {
DEFAULT: 'rounded-lg shadow-lg',
hover: isActive && 'bg-blue-500 scale-110',
placeholder: isActive && 'text-dark',
'group-focus': open && 'ring-2 ring-green-400',
},
xl: [
'p-8 text-lg',
{
dark: isDarkMode && 'text-gray-200 bg-gray-900',
'group-active': open && 'bg-yellow-300',
},
],
});
// w-10 p-4 sm:bg-green-200 md:text-white md:bg-gray-800 lg:rounded-lg lg:shadow-lg
// xl:p-8 xl:text-lg xl:dark:text-gray-200 xl:dark:bg-gray-900
- Use with
twMerge
to additionally resolve conflicts
const overrideClassnames: TwModInput = 'sm:bg-yellow-100,text-light,text-center';
const overrideClassnames1: TwModInput = {
sm: ['bg-yellow-200', 'text-blue-200', 'text-left'],
};
const result = twMerge(twMod(
{
sm: 'bg-red-100,text-white,font-bold',
},
overrideClassnames,
overrideClassnames1,
);
// sm:font-bold sm:bg-yellow-200 sm:text-blue-200 sm:text-left
- Use a custom instance of twMod to include the custom modifiers you've added to your Tailwind configuration.
Add a new breakpoint in tailwind configuration
/** @type {import('tailwindcss').Config} */
module.exports = {
theme: {
extend: {
screens: {
'3xl': '1600px',
},
},
},
plugins: [],
};
Create new instance of TwMod with default configuration
const twModCustom = createTwModWithDefaults({
customModifiers: ['3xl'],
});
twModCustom({
DEFAULT: 'w-10',
'3xl': 'w-50',
});
// w-10 3xl:w-50
Configuration
-
customModifiers
: Define custom modifiers to incorporate additional values when extending your Tailwind configuration, such as introducing a new breakpoint.
Support
- Compatible with all versions of Node.js.
- Works in all browsers that support ECMAScript 5 (ES5).
Editor Support
For enhanced auto-completion of tailwind utility classes in the twMod function, add the following configuration to your setup
(assuming the tailwind-language-server is installed):
VS Code
Add following settings to your `settings.json` file.
{
"tailwindCSS.experimental.classRegex": [
["twMod[a-zA-Z_]*\\(([^)]*)\\)", "(?:'|\"|`|,)*([^'\"`,]*)(?:'|\"|`|,)"]
]
}
Neovim
nvim-lspconfig
["tailwindcss"] = function()
lspconfig["tailwindcss"].setup({
settings = {
tailwindCSS = {
experimental = {
classRegex = {
{ "twMod[a-zA-Z_]*\\(([^)]*)\\)", "(?:'|\"|`|,)*([^'\"`,]*)(?:'|\"|`|,)" },
},
},
},
},
},
end
tailwind-tools
server = {
settings = {
experimental = {
classRegex = {
{ "twMod[a-zA-Z_]*\\(([^)]*)\\)", "(?:'|\"|`|,)*([^'\"`,]*)(?:'|\"|`|,)" },
},
},
},
}
Top comments (0)