Skip to main content

2026-07-14

🔗 The joy of extending Tailwind

One of the biggest upgrade that came with Tailwind 4 has been the new CSS-based configuration and extensibility system.

The CSS configuration came with a new simple syntax, which is very close to the way regular CSS is written. This makes it really easy to integrate Tailwind into a project's design system.

Let's consider a regular CSS class.

.my-class {
  display: block;
  color: red;
}

This is a perfectly fine class, and here is the same class registered as a tailwind utility using the new directive @utility.

@utility my-class {
  display: block;
  color: red;
}

Registering a class as a @utility brings super-powers to that class, together with nice improvements in developer experience.

  • You can now apply that class arbitrarily whenever the design requires it, whether it is desktop-only md:my-class, on hover hover:my-class, when in dark-mode dark:my-class, and many others.
  • The class is added to the final stylesheet only in case the utility is actually used, avoiding unnecessary CSS.
  • You can hover a class to lookup its CSS declarations, thanks to the amazing VSCode tailwind-intellisense extension.

A screenshot of a CSS class definition

# Props in CSS

There aren't only static @utility, Tailwind v4 introduced a whole CSS-like syntax to pass values into the utilities it generates. These props can modify the values of the CSS properties within the class, and they can be numbers, colors, percentages, keywords, etc.

It's pretty straightforward to grasp how the new functions work if you are already familiar with CSS syntax and logic. The main functions are --alpha(), --value(), --modifier(), --default().

Check out the official documentation on how to add custom styles, to see how these functions work together, and how they allow to generate complex components on-the-fly. Here are a few examples.

# CSS shapes

Using clip-path and the modern CSS function polygon(), together with a size prop, we can generate any sort of CSS-only shapes.

@utility star-* {
	width: calc(--value(number) * var(--spacing));
	height: calc(--value(number) * var(--spacing));
	clip-path: polygon(50% 0%, 61% 35%, 98% 35%, 68% 57%, 79% 91%, 50% 70%, 21% 91%, 32% 57%, 2% 35%, 39% 35%);
	flex-shrink: 0;
}

# A colored shadow border that blends in

If you are interested in subtle CSS details that make a UI feel polished, you will have probably encountered the border-shadow trick.

.border-shadow {
	box-shadow:
		0px 0px 0px 1px rgba(0, 0, 0, 0.06),
		0px 1px 2px -1px rgba(0, 0, 0, 0.06),
		0px 2px 4px 0px rgba(0, 0, 0, 0.04);
}

But now you can have this technique work with any color, and with any opacity modifier.

@utility border-shadow-* {
  box-shadow:
    0px 0px 0px 1px --alpha(--value(--color-*, [color]) / --modifier([percentage], 6%)),
    0px 1px 2px -1px --alpha(--value(--color-*, [color]) / --modifier([percentage], 6%)),
    0px 2px 4px 0px --alpha(--value(--color-*, [color]) / calc(--modifier([percentage], 6%) * 0.66));
}

# A conditional rounded box

Some time ago a post by Ahmad Shadeed gained popularity. It described a CSS one-liner which conditionally rounded an element depending whether the element touched the viewport (or its container) or not.

But for example, with tailwind we can have the one-liner also control how rounded the element initially was.

@utility conditional-rounded-* {
	border-radius: calc(sign(100cqi - 100%) * --value(number) * var(--spacing));
}

# Creating variants

The last chapter of extending tailwind touches the modifiers, which specify when applying a class.

These use another new directive @custom-variant. Tailwind already offers variants for the most used CSS media queries. But creating new ones can still be valuable for a few reasons:

  • Ergonomics, create shorter, more clear directives
@custom-variant touch {
	@media (pointer: coarse) {
		@slot;
	}
}
  • Patch browsers shortcomings, with ad-hoc inline fixes
@custom-variant safari {
	@supports (font: -apple-system-body) {
		@slot;
	}
}
  • Combining multiple directives together
@custom-variant hocus {
  @media (hover: hover) {
    &:hover {
      @slot;
    }
  }
  &:focus-visible {
    @slot;
  }
}

# Utility-driven agents

It's funny to feel joy in polishing code snippets, while being told you shouldn't even read code anymore.

But I think even with AI writing all the code, it's still valuable to produce abstractions that makes frontend code more expressive and clear. (Granted, you have to like Tailwind)

Ultimately if it's good for humans, it's good for LLMs too.

Also, I still prefer burning tokens on crafting powerful abstractions that I, the LLMs, and my team can reuse over and over, rather than painfully steering a 100B parameter model into a writing a flex column that takes 2 seconds to type.

Recently I was getting annoyed at how long it was taking me to position content inside the typical 12-columns layout, and having it adapt to the breakpoints.

Once I had the right idea, it really took one rant prompt and pointing the LLMs to the documentation page.

so i use grid with tailwind css but i get tired of writing a ton of col-start-2 col-span-3 md:col-start-4 md:col-span-6 etc etc

i want to create a set of @utility that allow me to write for example md:col-2-to-10

tailwind allows you to create custom utilities with a single parameter not two, so let's use the parameter only for the second column so @utility col-1-* and then create twelve for col-1-to-1, col-1-to-2, col-1-to-3 etc etc, if possible make the css valid only if * is at least the hardcoded value.

And it cooked exactly what I needed to fly through my front-end build.

@utility col-1-to-* {
  grid-column: 1 / calc(--value("1", "2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12") + 1);
}
 
@utility col-2-to-* {
  grid-column: 2 / calc(--value("2", "3", "4", "5", "6", "7", "8", "9", "10", "11", "12") + 1);
}
 
@utility col-3-to-* {
  grid-column: 3 / calc(--value("3", "4", "5", "6", "7", "8", "9", "10", "11", "12") + 1);
}

...

PS. I have a few other takes on this topic that I would like to write about, so if you would like to get notified when these posts come out, sign up with with the form below the post to receive it. There is an RSS feed too.

# Resources