How Utility Classes Become CSS

Tailwind CSS · Lesson 3 of 27 8 min read Updated

Tailwind can feel like magic: you type p-4 in your HTML and padding appears. Understanding what really happens makes you much faster at fixing problems, such as a class that "does nothing" or a heading that looks like plain text.

In this lesson you will follow a class from your HTML to the final CSS file, learn how to read any Tailwind class name, meet the CSS variables behind every utility, and see what Tailwind's base reset, called Preflight, does to normal HTML.

From class name to CSS rule

When the build runs, Tailwind does three things:

  1. Scans your files. It reads your HTML, JavaScript, PHP and other template files as plain text, looking for anything that could be a class name.
  2. Generates CSS for the matches. For every token it recognises, such as p-4 or md:p-8, it writes the CSS rule. Tokens it does not recognise are ignored.
  3. Writes one stylesheet. Only the classes you used are included, so the file stays small no matter how many utilities Tailwind offers.

Suppose a page contains this paragraph:

HTML
<p class="p-4 text-sky-700 hover:underline md:p-8">Results are out!</p>

Tailwind 4.3 generates these utility rules for it:

CSS
@layer utilities {
  .p-4 {
    padding: calc(var(--spacing) * 4);
  }
  .text-sky-700 {
    color: var(--color-sky-700);
  }
  @media (hover: hover) {
    .hover\:underline:hover {
      text-decoration-line: underline;
    }
  }
  @media (width >= 48rem) {
    .md\:p-8 {
      padding: calc(var(--spacing) * 8);
    }
  }
}

Notice three details. Every class is a normal CSS class selector. The colon in hover:underline is escaped as \: because a colon has a special meaning in selectors. And variants such as hover: and md: become a pseudo-class or a media query wrapped around the same declaration.

Every utility reads a CSS variable

The rules above do not contain fixed numbers. p-4 uses var(--spacing), and text-sky-700 uses var(--color-sky-700). These are theme variables: CSS custom properties that Tailwind defines once, at the top of the file:

CSS
@layer theme {
  :root, :host {
    --color-sky-700: oklch(50% 0.134 242.749);
    --spacing: 0.25rem;
  }
}

So p-4 is 4 × 0.25rem = 1rem (16px), and p-8 is 2rem. Because the values are variables, you can change the whole design from one place (see Customising with @theme), and you can use them in your own CSS, for example border-color: var(--color-sky-700);. Tailwind only outputs the variables your classes actually use.

Reading a class name

Most Tailwind classes follow the same pattern. Here is a long one taken apart: md:hover:bg-sky-700/50.

PartExampleMeaning
Variantsmd:hover:Conditions: on screens 48rem (768px) and wider, while hovered
UtilitybgWhich property: background colour
Valuesky-700Which value from the theme
Modifier/50An extra tweak: 50% opacity

Some classes are a single word with no value, such as flex, hidden, underline or italic. Some allow a negative value with a leading minus: -mt-2 pulls an element up. Once you see the pattern, unfamiliar classes become easy to guess.

Cascade layers keep things in order

The generated file is organised into four cascade layers (a CSS feature that controls which rules win; see Cascade Layers). Tailwind declares them in this order:

CSS
@layer theme, base, components, utilities;
  • theme holds the CSS variables.
  • base holds Preflight, the reset for plain HTML elements.
  • components is an empty layer for your own component classes.
  • utilities holds the utility classes.

Later layers beat earlier ones, whatever the selector specificity. That is why text-sky-700 always overrides the base styles for a link, and why utilities beat anything you put in the components layer.

Preflight: a clean starting point

Preflight removes the browser's default styling so every element starts from the same plain state. After Preflight:

  • Headings (<h1> to <h6>) have the same size and weight as normal text.
  • Paragraphs, headings and lists lose their default margins.
  • Lists lose their bullets and numbers.
  • Links inherit their colour and have no underline.
  • Images and videos become display: block and never grow wider than their container.
  • Every element uses box-sizing: border-box, so padding and border are included in the width.

Run this example. The left column has no classes, so you see the Preflight defaults; the right column adds utilities back on purpose.

HTMLTry it yourself
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <title>Preflight</title>
  <script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="grid gap-6 p-6 sm:grid-cols-2">
  <section class="rounded-lg border border-slate-300 p-4">
    <h1>Pune Book Fair</h1>
    <p>Stalls open at 10 am.</p>
    <ul>
      <li>Novels</li>
      <li>Exam guides</li>
    </ul>
    <a href="#">See the map</a>
  </section>
  <section class="rounded-lg border border-slate-300 p-4">
    <h1 class="text-2xl font-bold">Pune Book Fair</h1>
    <p class="mt-2">Stalls open at 10 am.</p>
    <ul class="mt-2 list-disc pl-5">
      <li>Novels</li>
      <li>Exam guides</li>
    </ul>
    <a href="#" class="mt-2 inline-block text-sky-700 underline">See the map</a>
  </section>
</body>
</html>

The reset can surprise you at first, but it means that text-2xl gives exactly the size you asked for, with no hidden browser margin or font size to fight.

Class names must appear in full

Because Tailwind only reads your files as text and never runs your code, it can only find class names that appear complete somewhere. This is the most common Tailwind bug:

JavaScript
// Wrong: the text "bg-green-600" never appears in the file
const status = 'green';
badge.className = `bg-${status}-600 text-white`;

// Right: every possible class name is written in full
const colours = { paid: 'bg-green-600', due: 'bg-amber-500', late: 'bg-red-600' };
badge.className = `${colours[invoice.state]} text-white`;

In the wrong version the browser receives the class bg-green-600, but Tailwind never saw that string, so no CSS was generated for it. In the right version all three class names exist in the source as complete words.

Common mistakes

  • Building class names from pieces. text-${size} or 'bg-' + colour cannot be detected. Write full class names and choose between them.
  • Two classes for one property. class="p-4 p-8" does not mean "the last one wins". The order of rules in the CSS file decides, not the order in the attribute. Use one.
  • Thinking an <h1> is broken. Preflight makes headings plain on purpose. Add text-3xl font-bold or similar.

Key points

  • Tailwind scans your files as text and generates CSS only for the class names it finds.
  • Variants such as hover: and md: become pseudo-classes and media queries around the same declaration.
  • Utilities read theme variables: p-4 is calc(var(--spacing) * 4) and --spacing is 0.25rem.
  • The output uses the layers theme, base, components and utilities; utilities win.
  • Preflight resets margins, heading sizes, list bullets and link colours so you start from a clean slate.
  • Class names must appear in full in your source; never build them by joining strings.

Exercise

A developer in Kochi wrote this code for a button that can be small or large, but neither size works:

JavaScript
const size = 'lg';
button.className = `text-${size} px-${size === 'lg' ? 6 : 3} rounded bg-sky-600`;

Explain why the size classes are missing, then rewrite the code so Tailwind can find every class.

Show solution

Tailwind never runs the JavaScript, so it never sees text-lg, px-6, text-sm or px-3 written in full, and generates no CSS for them. Write each complete class list and choose one:

JavaScript
const sizes = {
  sm: 'text-sm px-3 py-1',
  lg: 'text-lg px-6 py-3',
};
const size = 'lg';
button.className = `${sizes[size]} rounded bg-sky-600 text-white`;

Now every class appears as a full word in the file, so the build generates all of them.

Quick Quiz

1. How does Tailwind decide which CSS to generate?

2. What does p-4 produce in Tailwind 4.3?

3. Why does a plain <h1> look like normal text in a Tailwind page?

4. Which layer wins when a utility and a base style set the same property?

Learn Tailwind CSS with a mentor

Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.

  • Project-based learning
  • Mentor code reviews
  • Free counselling call