Capítulo 10 de 29

Chapter 10: Styling with Utility Classes

Core Idea

Tailwind's whole approach is composing many small single-purpose utility classes directly in markup instead of writing custom CSS with named selectors — this trades "thinking in CSS files" for "thinking in HTML classes," and is deliberately more constrained than inline styles because utilities pull from a shared design system and support states/media queries that inline styles can't.

Key Concepts

  • Why not just inline styles?: utilities look similar to inline styles but add constraints (values come from the theme, not magic numbers), and support things inline styles fundamentally cannot: state variants (hover:, focus:) and media/container queries (md:, @md:).
  • Variants only ever apply their own condition: hover:bg-sky-700 generates &:hover { background-color: ... } — the class does nothing unless hovered. This is the opposite of a traditional .btn class that bundles base + hover styles together; in Tailwind you always see both states as separate classes on the element itself. Variants stack (disabled:hover:bg-sky-500).
  • Composition via CSS variables: utilities that share an underlying CSS property (e.g. all filter utilities: blur-sm, grayscale) each set only their own CSS variable, and the property's value references all of them (falling back to nothing when unset) — so blur-sm grayscale combines cleanly instead of one overwriting the other. Same mechanism powers gradients, shadow colors, and transforms.
  • Arbitrary values recap: bg-[#316ff6] for one-off values, grid-cols-[24rem_2.5rem_minmax(0,1fr)] for complex values, calc() combined with theme functions (max-h-[calc(100dvh-(--spacing(6)))]), and fully arbitrary CSS variable properties ([--gutter-width:1rem]).
  • Why arbitrary values work: Tailwind isn't a static stylesheet — it scans your project text for class-shaped tokens and generates only the matching CSS at build time, so it can generate CSS for a value it's never seen before as long as the literal class string exists in source (see Detecting Classes in Source Files).
  • Complex/stacked selectors: variants chain directly (dark:lg:data-current:hover:bg-indigo-600); group/group-hover: styles a descendant based on an ancestor's state (mark the ancestor group, target descendants with group-hover:); arbitrary variants ([&>[data-active]+span]:text-blue-600) cover selectors with no built-in variant, especially useful when you don't control the surrounding HTML.
  • When inline styles still make sense: values from a dynamic runtime source (a DB/API-driven color), or an arbitrary value too complex to read as a class name — in both cases, a common pattern is setting a CSS custom property via style={{ '--bg-color': buttonColor }} and then consuming it with a utility (bg-(--bg-color)), keeping the state/variant behavior utilities give you while sourcing the raw value dynamically.
  • Managing duplication — in order of preference: (1) if the repeated markup is already rendered in a loop (map/each), there's no real duplication to solve — the class list is authored once; (2) multi-cursor editing for duplication localized to one file — often the best answer, no abstraction needed; (3) components/template partials when styles repeat across files (React/Vue/Svelte component, or Blade/ERB/Twig partial) — single source of truth; (4) a hand-written @layer components class (e.g. .btn-primary built from theme variables) only when a template partial feels like overkill for something trivial, and only for single-element cases — prefer partials for anything more complex.
  • Conflicting utilities — last one in the stylesheet wins, not the last one in the class attribute — so never apply two utilities that target the same property to the same element; branch the class value instead (e.g. gridLayout ? "grid" : "flex"), or expose a prop from a component rather than letting consumers add conflicting classes from outside.
  • ! important modifier: append ! to a class (bg-red-500!) to force !important on just that utility's declarations, for cases with no other way to win a specificity fight.
  • important import flag: @import "tailwindcss" important; marks every generated utility !important — useful when integrating Tailwind into a project with existing high-specificity legacy CSS.

Code Examples

// dynamic value via CSS variable + utility, keeps hover/variant support
<button
  style={{ "--bg-color": buttonColor, "--bg-color-hover": buttonColorHover }}
  className="bg-(--bg-color) hover:bg-(--bg-color-hover) ..."
>
  {children}
</button>

<a href="#" class="group rounded-lg p-8">
  <span class="group-hover:underline">Read more…</span>
</a>
  • What it demonstrates: bridging a dynamic/runtime value into the utility system without losing state-variant support, and the group/group-hover: parent-state pattern.

Key Takeaways

  1. Never stack two utilities targeting the same CSS property on one element — resolve the choice in your markup/component logic instead, since cascade order (not class order) decides the winner.
  2. Reach for the duplication ladder in order: loop > multi-cursor edit > component/partial > hand-written @layer components class — don't jump straight to a custom CSS class.
  3. ! (per-class) and the important import flag (global) exist for specificity fights, not as a default habit — both are last resorts.
  4. Composable multi-class properties (filter, transform, shadow, gradient) work because each utility only touches its own CSS variable — this is why blur-sm grayscale combines instead of one canceling the other.

Connects To

  • Hover, Focus & Other States: variants (hover:, group-hover:, arbitrary variants) are covered in full there.
  • Adding Custom Styles: @layer components and arbitrary-value/property/variant syntax are documented in depth there.
  • Detecting Classes in Source Files: explains exactly how/why plain-text scanning makes arbitrary values possible.