Skip to content

BEM and Tailwind: One Job Each

FieldValue
TypeSkill Resource
Source~/.copilot/skills/design/references/css/bem-and-tailwind.md
DescriptionNot specified

Source Content

BEM and Tailwind: One Job Each

BEM and Tailwind are not rivals. They solve different problems and live side by side. The whole model fits in one sentence: BEM owns what a thing is; Tailwind owns how it sits. Every value either of them emits ultimately comes from a token, so the visual language stays consistent no matter which tool wrote the rule (see tokens-and-theming.md).

On this page

  • The two approaches, each on its own terms
  • The rule: don’t make them fight
  • Responsibility → owner (decision table)
  • BEM naming rules
  • When to promote utility soup into a BEM class

The two approaches, each on its own terms

BEM — component ownership and semantic structure

Reach for a named BEM class when you are defining a reusable component API or styling something that carries meaning: a variant, a state, a skin, a semantic part of a component. The class name is the contract — site-nav__link--active says what the element is and what state it is in, in markup, with no comment needed. This is the home for anything another developer will reuse or reason about by name.

<!-- BEM owns the component's identity and meaningful states. -->
<!-- The class names are the API; read them and you know the structure. -->
<nav class="site-nav">
<a class="site-nav__link site-nav__link--active" href="/">Home</a>
<a class="site-nav__link" href="/about">About</a>
</nav>
/* site-nav.css ───────────────────────────────────────────────
Block: the navigation component. Owns its skin + link states.
Layout/spacing live in the markup as utilities — not here. */
.site-nav__link {
color: var(--color-fg-muted); /* token, never a hex */
border-radius: var(--radius-md);
}
/* Modifier = a meaningful state, owned by BEM, defined once. */
.site-nav__link--active {
color: var(--color-fg);
background: var(--color-bg-subtle);
}

Tailwind utilities — layout, spacing, and one-off visual adjustments

Reach for utilities for the common styling that does not deserve a named abstraction: flex/grid, gaps, padding, alignment, a single non-recurring tweak. These describe arrangement, not identity. The same nav above gets its layout from utilities, and that layout never needs a name because nothing reuses it as a concept.

<!-- Tailwind owns arrangement: how the links flow and breathe. -->
<!-- Symmetric padding (px == both sides, py == top/bottom). -->
<nav class="site-nav flex gap-4 px-6 py-3">
<a class="site-nav__link site-nav__link--active" href="/">Home</a>
<a class="site-nav__link" href="/about">About</a>
</nav>

The Tailwind classes (flex gap-4 px-6 py-3) and the BEM classes (site-nav, site-nav__link--active) sit on the same elements without overlap. Each answers a question the other never touches.

The rule: don’t make them fight

Don’t make Tailwind fight BEM, and don’t make BEM recreate Tailwind.

The single failure mode is defining one responsibility twice. If a button’s primary variant is owned by a BEM modifier, it must not also be spelled out in raw Tailwind color utilities — now there are two sources of truth for “what does primary look like,” and they will drift.

<!-- MUDDY ── the variant is defined twice. -->
<!-- button--primary already means "primary skin"; bg-blue-600 text-white
re-spells that skin in utilities AND hardcodes a raw color.
Two owners for one responsibility. They will disagree later. -->
<button class="button button--primary bg-blue-600 text-white">Save</button>
<!-- CLEAN ── one owner each. -->
<!-- BEM owns the variant (button--primary -> primary skin from tokens).
Tailwind owns layout/spacing only (symmetric px-4 py-2). -->
<button class="button button--primary px-4 py-2">Save</button>

In the clean version, “what primary looks like” lives in exactly one place — the .button--primary rule, reading color from a token. To change primary everywhere, you edit that one rule. The utilities say nothing about identity; they only place and pad.

Responsibility → owner

ResponsibilityOwnerWhy
Component variant (primary, ghost, danger)BEM modifierA named, reusable skin — the component’s API
Component state (active, disabled, loading, selected)BEM modifierMeaningful state, reasoned about by name
Semantic part of a componentBEM element (block__element)Names the structure for reuse
Layout (flex, grid, gap)Tailwind utilityArrangement, not identity — never reused as a concept
Padding / internal spacingTailwind utilitySymmetric, situational, no name needed
Positioning, alignmentTailwind utilityOne-off arrangement
Outer spacing between siblingsTailwind at the layout parentMargins belong to layout, not the component — see spacing.md
One-off visual tweak (single use)Tailwind utilityNot worth a named class
Design values (color, radius, type, space)Tokens, behind bothThe shared source both tools read from — see tokens-and-theming.md

The last row is the load-bearing one: a BEM rule and a Tailwind utility that style the same property should resolve to the same token. Tailwind utilities are configured to emit token values (see tailwind-config.md), and BEM rules use var(--token) directly. Neither tool invents a raw value.

BEM naming rules

  • Block — the standalone component: .site-nav, .button, .card. One block per file; the file is named after the block (site-nav.css).
  • Element — a part that has no meaning outside its block: .site-nav__link, .card__title. Double underscore.
  • Modifier — a variant or state of a block or element: .button--primary, .site-nav__link--active. Double hyphen.
  • One block per file. The block’s whole API lives in one place, easy to find and edit.
  • No deep nesting. Names stay flat: block__element--modifier, never block__element__subelement. If you reach for a grandchild, the child is probably its own block.
  • Style by class, not by element or descendant. .card__title, not .card h2 — selectors stay shallow, specificity stays flat, and the markup is free to change.

When to promote utility soup into a BEM class

Most utilities should stay in the markup. Promote a cluster of utilities into a named BEM class only when one of these is true:

  • It repeats 3+ times. The same run of utilities copy-pasted across three or more elements is duplication waiting to drift — give it a name and define it once. (Sharing over duplication is a core rule; see architecture.md.)
  • It represents a named component concept. If the cluster means something a developer would refer to by name — “this is the card header,” “this is the primary action” — it has earned a BEM class even on first use, because the name is the documentation.

If neither is true, leave the utilities inline. A single, non-recurring arrangement does not deserve an abstraction — premature promotion is its own kind of complexity. Keep it simple: name things that have meaning or repeat, and nothing else.