Skip to main content

The utility-first mindset

In this chapter, you understand "utility-first," the core idea of Tailwind CSS.

What is a utility class

A utility class is a class that applies a single CSS property.

<!-- Each class is responsible for one property -->
<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm">
...
</div>
ClassCorresponding CSS
flexdisplay: flex
items-centeralign-items: center
justify-betweenjustify-content: space-between
p-4padding: 1rem
bg-whitebackground-color: #fff
rounded-lgborder-radius: 0.5rem
shadow-smbox-shadow: ...

Note that v3's shadow was renamed to shadow-sm in v4 (the shadow scale is shifted by one step).

Comparison with traditional CSS

The traditional approach

styles.css
.card {
display: flex;
align-items: center;
justify-content: space-between;
padding: 1rem;
background-color: white;
border-radius: 0.5rem;
box-shadow: 0 1px 3px rgba(0, 0, 0, 0.1);
}
<div class="card">...</div>

Utility-first

<div class="flex items-center justify-between p-4 bg-white rounded-lg shadow-sm">
...
</div>

Why utility-first

1. You do not have to think about class names

With traditional CSS, it takes time to come up with appropriate class names.

/* What should I name this? */
.card-wrapper { }
.card-container { }
.card-outer { }
.product-card-layout { }

With utility classes, you just write the styles directly.

2. The CSS does not bloat

With the traditional approach, the CSS keeps growing as the project progresses.

/* Similar styles proliferate */
.card { padding: 1rem; }
.card-large { padding: 1.5rem; }
.card-small { padding: 0.5rem; }
.product-card { padding: 1rem; }
.user-card { padding: 1rem; }

With Tailwind CSS, only the classes you use are emitted.

3. Changes are safe

With traditional CSS, changing one class can affect other places.

/* Is this change safe? */
.card {
padding: 1rem; /* → what happens if I change it to 2rem... */
}

Utility classes are scoped to each element, so the scope of impact is clear.

4. Enforcing a design system

Because Tailwind CSS uses predefined values, you can maintain a consistent design.

<!-- Choose from a fixed scale -->
<div class="p-1">4px</div>
<div class="p-2">8px</div>
<div class="p-4">16px</div>
<div class="p-8">32px</div>

<!-- Arbitrary values are also possible, but not recommended -->
<div class="p-[17px]">17px</div>
Keep arbitrary values to a minimum

Arbitrary values (p-[17px]) are convenient, but they cause your design consistency to break down. First consider whether you can handle it with Tailwind's scale (p-4 = 16px, p-5 = 20px). In v4 you can specify an integer that is not on the scale even without brackets, as in p-17, but note that p-17 becomes 17 × 0.25rem = 4.25rem (not 17px). Either way, overusing off-scale values breaks design consistency.

Naming convention

Tailwind CSS class names follow an intuitive convention.

Pattern 1: property-value

<div class="text-center"> <!-- text-align: center -->
<div class="font-bold"> <!-- font-weight: bold -->
<div class="bg-blue-500"> <!-- background-color: ... -->

Pattern 2: property-direction-value

<div class="p-4"> <!-- padding: 1rem (all sides) -->
<div class="px-4"> <!-- padding-left/right: 1rem -->
<div class="pt-4"> <!-- padding-top: 1rem -->
<div class="mt-4"> <!-- margin-top: 1rem -->

Pattern 3: condition-property-value

<div class="hover:bg-blue-600"> <!-- on hover -->
<div class="md:flex"> <!-- 768px and up -->
<div class="dark:bg-gray-800"> <!-- dark mode -->

The spacing scale

Tailwind CSS uses a 4px-based scale.

ClassValue
00px
14px (0.25rem)
28px (0.5rem)
312px (0.75rem)
416px (1rem)
624px (1.5rem)
832px (2rem)
1248px (3rem)
1664px (4rem)

The "HTML gets long" problem

When you use utility classes, the HTML can get long.

<button class="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
Button
</button>

Solution 1: Componentization

Button.tsx
function Button({ children }: { children: React.ReactNode }) {
return (
<button className="inline-flex items-center justify-center px-4 py-2 text-sm font-medium text-white bg-blue-600 border border-transparent rounded-md shadow-sm hover:bg-blue-700 focus:outline-hidden focus:ring-2 focus:ring-blue-500 focus:ring-offset-2">
{children}
</button>
)
}

// Usage
<Button>Click</Button>

Solution 2: The @apply directive

/* Use sparingly. You can use it when necessary */
.btn-primary {
@apply inline-flex items-center justify-center px-4 py-2;
@apply text-sm font-medium text-white;
@apply bg-blue-600 hover:bg-blue-700;
@apply rounded-md shadow-sm;
}
Use @apply sparingly

Overusing @apply undermines the benefits of utility-first, so prefer componentization (Solution 1). When you need a component-level abstraction, also consider the @utility directive introduced in Chapter 16 / Customization.

v4's @apply has a file-scope caveat

In v4, @apply has a file-scope constraint. When you use it outside the main CSS file (in CSS Modules or in the <style> block of Vue/Svelte, etc.), you need to reference the file that defines the theme with the @reference directive.

/* When using @apply in a separate file */
@reference "../../styles/main.css";

.my-component {
@apply text-blue-500;
}

Summary

  • A utility class applies a single CSS property
  • You do not think about class names, and you prevent CSS bloat
  • Changes are safe, and you maintain design consistency
  • The regular naming makes it easy to learn
  • Solve long class lists with componentization