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