Going back from all-utilities to plain CSS for styling buttons has made my life easier.

Please note: I have zero interest in taking part in that dreaded love/hate Tailwind argument that cooks up every other month on social media. These are just some reflections about how I’ve been doings thing for years, and what I consider changing.

Starting with buttons

As a part of reevaluating the use of utility CSS in various use cases, the ubiquitous button component is one of the first instances where I’ve decided to go back to vanilla CSS. It has become tedious to change things, add additional variants or add template logic.

So I’ve removed those utilities from the button component’s markup:

Git diff showing how utility classes were replaced with (content-derived) semantic class name

I’ve just moved those rules in a file called button.css , as a first step by just dumping all rules into the CSS using Tailwind’s @apply . (I’m aware that using @apply is not the best or even encouraged way, but it’s a good bridge to move quickly).

I came for better maintainability, I stayed for contextual overrides

The big bonus: By writing good old CSS it becomes very simple to override a button variant based its context. This saves a lot of template logic or editor work. The cascade is, of course, as old as CSS itself, but I confess I kind of forgot about its possible usefulness.

Example: Change button appearance if used inside a container with dark background

Say this is our default styling for a primary and secondary button:

.button {
    @apply font-semibold inline-flex items-center border-2 gap-2 py-4 px-6 rounded-full leading-none no-underline select-none motion-safe:transition hover:shadow-lg active:translate-y-0.5;

.button--primary {
    @apply bg-primary text-white border-primary hover:shadow-primary-600/30 focus-green;

.button--secondary {
    @apply text-blue-950 border-cream-200 hover:bg-white/30 hover:shadow-cream-600/20;

This creates a secondary button with a transparent background, dark text and a subtle border. It work’s great on a light background:

Screenshot showing a button on a with dark text on transparent background

But when put on a dark background, it fails:

Screenshot showing a button on a with dark text on transparent background, but now placed on a dark blue background. The button becomes hard to read.

Now, we could create a new button variant. Then we’d write some template logic to use that new variant at the right time, such as when the button is placed inside a certain component. Or we put the burden on the editor and make them choose the correct variant in the CMS. Then they need to choose a button variant based on hierarchy and visual design. Both makes things more complicated. Let alone that we’re still talking about a secondary-level button from a UX hierarchy.

This is a perfect use case to embrace the cascade and just override the secondary button’s default styling – depending on the component it’s used within:

/* Buttons on dark background need some overrides */
[data-component="cta"] {
    .button--secondary {
        @apply bg-white border-white focus-white;

And we’re done.

The once hard to read button now has a white background and is readable against a dark background

This seems almost too obvious and I’m really curious where this journey will take me 😊