C.W.K.
Stream
Lesson 03 of 07 · published

@layer — Organizing CSS Without Specificity Wars

~12 min · css, layer, cascade, tailwind

Level 0React Novice
0 XP0/54 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
The cascade has historically been an arms race of specificity. @layer is the native CSS feature that ends the war by giving you explicit cascade ordering. Tailwind v4 builds on it.

What @layer is

@layer creates named groups in the cascade. Rules in a later-named layer beat earlier ones regardless of specificity. @layer reset, base, components, utilities; declares an explicit order — anything in utilities wins over anything in components, even if the components selector is longer.

Tailwind v4 uses three layers

When you write @import "tailwindcss", Tailwind sets up three layers internally: theme (your CSS variables from @theme), base (browser resets, defaults), components (slot for your custom component classes), utilities (the bg-brand, p-4 classes). Order is fixed; you add to the slots.

Why you'd add to a layer

Sometimes a Tailwind utility isn't enough — you need a named component class with multiple lines of CSS (e.g. an animation keyframe, a complex pseudo-element). Put it in @layer components so it sits in Tailwind's expected cascade position. Then in JSX you can still override with a utility (<button className="btn-primary px-6">) and Tailwind's utilities layer wins over your components layer.

The escape hatch

If you write a rule outside any layer, it lands after all layered rules in the cascade — meaning it beats everything by default (because unlayered rules are not in a layer). Avoid this for component CSS — use @layer components. Reserve it for genuine globals like third-party widget overrides where you need to win against the library's own rules.

Layers replace specificity hacks. If you've ever written .foo.foo.foo { color: red !important; } to win the cascade, layers are the answer. Put your rule in the right layer and the cascade resolves cleanly.

Code

Custom button class in @layer components·css
@import "tailwindcss";

@theme {
  --color-brand: #FF8FBE;
}

@layer components {
  /* A reusable button class. Tailwind utilities can still override. */
  .btn-primary {
    display: inline-flex;
    align-items: center;
    gap: 0.5rem;
    padding: 0.5rem 1rem;
    border-radius: 0.5rem;
    background: var(--color-brand);
    color: white;
    transition: opacity 0.15s;
  }
  .btn-primary:hover { opacity: 0.9; }

  /* Use in JSX: <button className="btn-primary">…</button>
     Override padding: <button className="btn-primary px-6">…</button>
     (Tailwind's px-6 in the utilities layer wins over our padding here.) */
}
An animation that needs @keyframes·css
@layer utilities {
  /* Utility class that pairs with @keyframes — both in the same layer. */
  .animate-pulse-brand {
    animation: pulse-brand 2s ease-in-out infinite;
  }
}

/* @keyframes don't live in layers — they're global identifiers. */
@keyframes pulse-brand {
  0%, 100% { opacity: 1; }
  50%      { opacity: 0.6; }
}

External links

Exercise

Add a custom .card class in @layer components that sets background, border, padding, and shadow. Use it in JSX. Then add Tailwind utility classes on the same element to override one of those properties (e.g. <div className="card p-8"> to override padding). Confirm the utility wins. Now move the .card rule outside @layer components and observe the utility no longer winning — that's the unlayered trap.
Hint
Test order: layer-component wins over layer-utility? NO — Tailwind sets components BEFORE utilities, so utilities win. That's the design. Unlayered rules win against both because they sit at the end of the cascade outside the layer order.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.