CSS

The CSS / Utility hybrid approach with Tailwind v4

Dane Grant
4 min read
Dane - The CSS hybrid approach with Tailwind v4
This article was written over 18 months ago and may contain information that is out of date. Some content may still be relevant, but please refer to official documentation for the latest information.

Just recently, the progress on the next major version of Tailwind CSS was open-sourced, along with a preview post. This is very exciting news to me, and I love the direction they’ve taken with this release. Tailwind can be a pretty polarizing topic among developers, but I believe that we can find some common ground and live in harmony. In this post, we’ll look at how the CSS + Tailwind hybrid approach is getting an upgrade in v4 and some of the patterns that we can take advantage of to provide a stellar styling experience on our websites.

The hybrid approach

It doesn’t matter what your personal take on Tailwind is, the fact is utility classes are an incredibly useful pattern for styling websites with CSS. I’m not sold on the hard-line, all-or-nothing approach that Tailwind has always recommended. Having a class string that extends way off the screen on an ultra-wide monitor is not pretty. Combining traditional CSS classes with utility classes helps cut down on the styling noise in your markup and provides some advantages when applied correctly. This post will cover some useful patterns and best practices for using CSS and Tailwind in harmony

Tailwind going “CSS-native”

In the v4 introduction post, they state

A major goal of Tailwind CSS v4.0 is making the framework feel CSS-native, and less like a JavaScript library.

I love that they are heading in this direction, and I’m imagining Tailwind as the toolkit for styling websites with CSS. It is going to replace the plethora of tools from the past and provide support for all the modern niceties like:

  • Importing CSS files with @import
  • Vendor prefixing (no more autoprefixr)
  • Improved browser support for modern features like CSS nesting and media query ranges.

This means less worry for us as developers. We can write our code and build our sites using the modern features that the platform provides without sweating all the other details and edge cases. The talented folks at Tailwind and the open-source community have our backs.

Tailwind v4 CSS configuration

The biggest step in the “CSS-native” direction and the feature that I’m most excited about is the CSS configuration. This is great for embracing CSS and Tailwind as one instead of two factions at battle. If you’ve been using Tailwind exclusively for a long time like I have, you might be out of touch with all the great features and tools that CSS provides out of the box now. This is a great opportunity to get back to learning and using the platform.

In Tailwind v4 we do our configuration and theming with CSS variables. To get an idea of what this looks like check out the default theme on GitHub. In our configuration we can override any values from the default theme or extend it with new values.

@import "tailwindcss";

@theme {
  --color-*: initial;

  --color-gray-50: #f8fafc;
  --color-gray-100: #f1f5f9;
  --color-gray-200: #e2e8f0;

  --color-green-800: #3f6212;
  --color-green-900: #365314;
  --color-green-950: #1a2e05;
}

All the values from the theme are exposed as CSS variables. We can reference them in our CSS files or directly in our utility classes.

.badge--red {
	background-color: var(--color-red-50);
	color: var(--color-red-600);
}
<div class="p-[calc(var(--spacing-6)-1px)]">
  <!-- ... -->
</div>

Best practices for combining CSS with utility classes

I can understand why Tailwind recommends using strictly utility classes. It’s easy to shoot yourself in the foot and run into the issues that utility classes set out to solve in the first place. I believe if you follow some general guidelines you can safely have the best of both worlds.

Create focused/specialized classes

CSS tends to get messy when we use classes that are overly specific with styles that apply to just one or a small number of use cases.

Take a calendar where the days are custom checkboxes. You might have some pretty specialized styles for this bit of UI.

.calendar-date {
	color: var(--color-gray-300);
	background-color: var(--color-gray-800);

	&:hover, 
	&:has(input[type="checkbox"]:focus) {
		background-color: var(--color-gray-700);
	}

	& input[type="checkbox"]:checked + time {
		background-color: var(--color-indigo-600);
		color: var(--color-white);
	}
}

Notice that we just add some specific styles that apply to this use case. This gives us the flexibility to keep composing our styles with utility classes.

Some things are better left to utilities

The best way to avoid wandering too far off the path of enlightenment is to stick with utility classes for certain (most?) things. Spacing and layouting are examples of things that are probably best left to utility classes. These typically vary all across a page, and encoding them in a class will make it a lot less flexible.

A code smell to look out for: If you have abstracted something to a class and you end up overriding certain aspects of its styles in different places. Those styles should probably be utility classes.

Classes as components

Tailwind has always recommended using a component system for your UI components so that your utility classes are all defined in a single place. You don’t want to have to edit 50 different files to make a change to the style of your buttons across your website. I’ve found that defining things that might be simple “components” like buttons, badges, links, etc as classes works just as well in most cases.

You can use Tailwind CSS with the @apply directive or traditional class definitions for this:

.badge {
	 @apply inline-flex items-center gap-x-1.5 rounded-md py-0.5 text-sm/5 font-medium sm:text-xs/5;
}

You can even bring the BEM naming convention back to compose variants on top of base component styles. (or name them however you’d like!)

.badge--yellow {
	@apply bg-yellow-400/20 text-yellow-700 ;
}

Now we can easily define badges in our markup:

<span class="badge badge--yellow">Warning</span>

Modern CSS and other interesting utility hybrid patterns

I think a lot of us are realizing that modern CSS is pretty great. There are a ton of awesome new features gaining support across major browsers, like CSS nesting, container queries, and more! More and more people are also coming around to the utility of utility classes. If you want to dive deeper and see other interesting approaches and patterns when combining modern CSS with utility classes, check out the post: Modern CSS patterns in Campfire.

Summary

I’m looking forward to the Tailwind CSS configuration and other awesome updates coming in v4. Using Tailwind doesn’t have to be a one-size-fits-all approach. If you want to write some CSS, I say go for it! Hopefully, some of the points in this post will lead to success if you decide to do so.

About the author

Dane Grant

Dane Grant

Senior Software Engineer

JavaScript developer building things on web, native, and server environments. Free time filled with reading, cooking, dog, sports, comedy, and music.

Keep reading

View all posts →

CSS Container Queries, what are they?

In this blog post, we take a look at container queries, a new feature that makes designing websites easier and more flexible. ...

Jesús Padrón6 mins
CSS

An example-based guide to CSS Cascade Layers

CSS Cascade Layers make style management simple—see how in this example-driven guide....

Dane Grant4 mins
CSS

Understanding the Difference Between `:focus` and `:focus-visible` in CSS

Understanding the Difference Between :focus and :focus visible in CSS I have learned my fair share about the importance of keyboard accessibility, so I know that visual indication of the focused element is very important. But the well known :focus pseudo class is not always the best fit for this job. That's where :focus visible comes in. Let's look at the differences between these two pseudo classes and explore the best practices for using them effectively. What is the :focus Pseudo Class? The :focus pseudo class is a CSS selector that applies styles to any element that receives focus, regardless of how that focus was triggered. This includes focus events from keyboard navigation, mouse clicks, and touch interactions. Example Usage of :focus In this example, the button will display a blue outline whenever it is focused, whether the user clicks on it with a mouse, taps it on a touchscreen, or navigates to it using the keyboard. What is the :focus visible Pseudo Class? The :focus visible pseudo class is more specialized. It only applies styles to an element when the browser determines that the focus should be visible. This typically occurs when the user navigates via the keyboard or assistive technologies rather than through mouse or touch input. Example Usage of :focus visible Here, the button will only show a blue outline when focused through keyboard navigation or another input method that usually requires visible focus indicators. Key Differences Between :focus and :focus visible :focus Behavior: Applies to any element that receives focus, regardless of the input method. Use Cases: Ensures that all interactions with the element are visually indicated, whether by mouse, keyboard, or touch. :focus visible Behavior: Applies styles only when the focus should be visible, such as using a keyboard or assistive technology. Use Cases: Ideal for scenarios where you want to provide focus indicators only to keyboard and assistive technology users while avoiding unnecessary outlines for mouse and touch users, typically required by design. Accessibility Implications :focus Pros: Guarantees that all users can see when an element is focused, which is critical for accessibility. Cons: Can lead to a suboptimal experience for mouse users, as focus styles may appear unnecessarily during mouse interactions. :focus visible Pros: Enhances user experience by showing focus indicators only when necessary, thus keeping the interface clean for mouse and touch users. Tailors the experience for keyboard and assistive technology users, providing them with clear visual cues. Cons: Additional considerations may be required to ensure that focus indicators are not accidentally omitted, especially in older browsers that do not support :focus visible. There may be cases where you want to show focus indicators for all users, regardless of input method. Best Practices for Using :focus and :focus visible To achieve the best accessibility and user experience, combining both :focus and :focus visible in your CSS is often a good idea. Combining :focus and :focus visible Here is a Stackblitz example of what such styling could look like for you to try out and play with. Additional Tips Test with Keyboard and Assistive Technology: Ensure that your web application is navigable using a keyboard (Tab, Shift + Tab, etc.) and that focus indicators are visible for those who rely on them. It's never a bad idea to include accessibility testing in your e2e testing suite. Provide Clear Focus Indicators: Make sure that focus indicators are prominent and easy to see. A subtle or hard to spot focus indicator can severely impact accessibility for users who rely on keyboard navigation. Conclusion The :focus visible pseudo class offers a more refined way to manage focus indicators, improving accessibility and user experience, particularly for keyboard and assistive technology users. By understanding the differences between :focus and :focus visible, and applying best practices in your CSS, you can create more accessible and user friendly web applications. Remember, accessibility should never be an afterthought. By thoughtfully applying focus styles, you ensure that all users, regardless of how they interact with your site, can easily navigate and interact....

Jan Kaiser3 mins
AccessibilityCSSPlaywright

How to Truncate Strings Easily with CSS

Learn how to truncate text in CSS, focusing on single-line and multi-line truncation using properties like overflow, text-overflow, and -webkit-line-clamp. It highlights CSS’s simplicity and responsiveness compared to JavaScript-based truncation....

Mark Shenouda4 mins
CSS