Organize your CSS in the Tailwind style with @layer directive

kinopyo avatar

kinopyo

Tailwind provides a @layer directive to help you better organize your CSS. This post is a detailed breakdown of the directive for my own learning purposes as I'm new to the concept. Tailwind provides a great guide on this topic, take a look at that if you are in a hurry.

I'll include several Tailwind Play links in this post. It allows you to see the generated CSS, which is very handy when it comes to debug how @layer works.

Tailwind 3 layers: base, components, and utilities

Let's cover the basics first.

@tailwind base;
@tailwind components;
@tailwind utilities;

base, components, utilities are 3 different "layers" in Tailwind, a concept popularized by ITCSS.

To quote from Tailwind documents:

  • The base layer is for things like reset rules or default styles applied to plain HTML elements.
  • The components layer is for class-based styles that you want to be able to override with utilities.
  • The utilities layer is for small, single-purpose classes that should always take precedence over any other styles.

@tailwind base includes the reset/normalized default styles and --tw-xxx CSS variables. Open https://play.tailwindcss.com/YpShH9YUHX?file=css and check out the full list at "Generated CSS" → "Base" tab.

@tailwind components is empty by default. We want to have our custom components like .btn, card, badge to be included at this layer.

@tailwind utilitites includes their shining utilities like text-gray-900, font-bold etc.

In CSS, the declaration order matters

To learn about @layer directive, we first need to understand what problem it tries to solve.

When two CSS classes have the same specificity, the one defined after wins.

Let's look at some examples. Here is a miss-configured Tailwind CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

.btn-blue {
  @apply bg-blue-500 text-white;
}

And we want to override the default blue button to green:

<button class="btn-blue bg-green-500">...</button>

But it won't work.

still blue
still blue

Here is the Tailwind Play link: https://play.tailwindcss.com/390II0ALLZ?file=css
You'll see these in the bottom of the generated CSS:

/* Tailwind base above… */

.bg-green-500 {
  /* ... */
}

.btn-blue {
  /* ... */
}
  • .bg-green-500 comes from @tailwind utilities.
  • .btn-blue is declared after .bg-green-500.

Since they have the same specificity, when they target the same property (background-color), .btn-blue wins.

Here comes one important lesson: components should be declared before utilitites.

To fix this, we can move .btn-blue CSS before @tailwind utilitites:

@tailwind base;
@tailwind components;

.btn-blue {
  @apply bg-blue-500 text-white;
}

@tailwind utilities;

The generated CSS:

/* Tailwind base above… */

.btn-blue {
  /* ... */
}

.bg-green-500 {
  /* ... */
}

https://play.tailwindcss.com/RoIyJ1XA1h?file=css

Green!
Green!

Now the button is green 🍏. We have overidden the background color with .bg-green-500 utility class.
That's great. Problem solved. Why do we need @layer?

The problem is now there is an order dependency. We need to remember to add new components before @tailwind utilities;. With @layer, it frees us from this dependency. (There is also another advantage of using @layer - it will remove unused CSS. More details later.)

@layer is like a portal 🚪

Let's see how we can use @layer to rewrite the CSS:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-blue {
    @apply bg-blue-500 text-white;
  }
}

It generates the same CSS https://play.tailwindcss.com/H3Tfc3aGsY?file=css:

/* Tailwind base above… */

.btn-blue {
  /* ... */
}

.bg-green-500 {
  /* ... */
}

Can we use multiple @layer components? Yes!

This is an example code provided in https://github.com/tailwindlabs/tailwindcss/pull/2312:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn { background: blue }
}

@layer utilities {
  .align-banana { text-align: banana }
}

@layer base {
  h1 { font-weight: bold }
}

@layer components {
  .card { border-radius: 12px }
}

@layer base {
  p { font-weight: normal }
}

@layer utilities {
  .align-sandwich { text-align: sandwich }
}

...conceptually becomes this:

@tailwind base;
h1 { font-weight: bold }
p { font-weight: normal }

@tailwind components;
.btn { background: blue }
.card { border-radius: 12px }

@tailwind utilities;
.align-banana { text-align: banana }
.align-sandwich { text-align: sandwich }

I like to imagine @layer as a portal. It teleports what you defined inside the block to the specified layer, giving you the freedom to organize your code the way you like as well as guarantee the final declaration order in the complied file.

https://web.dev/state-of-css-2022/#cascade-layers has an excellent animation demonstrating how layer works.


Now you know what problem @layer solves and how it works. Before you go, there is one more important thing to cover - understand how Tailwind "purge" unused CSS and how it may impact the layer usage.

Tailwind Purge Unused CSS (Tree-Shaking)

Let's reuse our previous example. We've correctly defined .btn-blue in the components layer:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  .btn-blue {
    @apply bg-blue-500 px-4 py-2 text-white;
  }
}

But let's remove the .btn-blue class from the markup:

<button class="">...</button>

Check out the "Components" tab under "Generated CSS" panel https://play.tailwindcss.com/j89riTHKBN, you'll see it's empty - .btn-blue got purged from the compiled CSS since it's not used!

Side note: The word "purge" is not techinically accurate anymore. Tailwind used to rely on postcss-purgecss to remove unused CSS. But Tailwind had implemented the Just-In-Time engine and made it the default behavior since Tailwind V3. Tailwind now no longer depends on postcss-purgecss and its previous purge option is renamed to content. For convenience though, I'll keep using "purge" to refer to this behavior.

Remember this setting in tailwind.config.js? You may see something like this in a typical Ralis app:

module.exports = {
  content: [
    "./app/views/**/*.html.erb",
    "./app/helpers/**/*.rb",
    "./app/javascript/**/*.js"
  ],

Tailwind will scan the files specified in content and track CSS that are in use. Any Tailwind CSS or our custom ones that are not used in those files will get removed from the generated CSS. This technique is called Tree Shaking. It is a core feature that keeps the compiled file size small as Tailwind ships a lot of utility classes.

This is from the official doc:

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  /* This won't be included in your compiled CSS unless you actually use it */
  .card {
    /* ... */
  }
}
@tailwind base;
@tailwind components;

/* This will always be included in your compiled CSS */
.card {
  /* ... */
}

@tailwind utilities;

Any custom styles you add to the base, components, or utilities layers will only be included in your compiled CSS if those styles are actually used in your HTML.
If you want to add some custom CSS that should always be included, add it to your stylesheet without using the @layer directive:
https://tailwindcss.com/docs/adding-custom-styles#removing-unused-custom-css

This behavior makes sense, right? In our previous example, there is no need to include .btn-blue in the genereated CSS since it's not used in the markup .

There is just this one gotcha you need to pay attention to - If you include your 3rd party CSS in layers, then they will be removed accidentally if they are not used in your markup.

What does it mean practically?

In my experience, if you use any backend/frontend libraries that render HTML with specific CSS, then including their CSS outside of layers is the best call.

For example, Rails Action Text renders HTML that contains classes like .trix-content.

<%= render post.content %>

This outputs HTML like below:

<div class="trix-content">
  Content...
</div>

Since trix-content CSS class does not exist in your own codebase, Tailwind won't know about it. And if you define it in the components layer, you'll lose it.

@tailwind base;
@tailwind components;
@tailwind utilities;

@layer components {
  /* This will be removed from the generated CSS since the class is not used explicitly */
  .trix-content {
    /* ... */
  }
}

The same applies to other third party libraries.

To avoid getting these library CSS removed by tree shaking, define them outside of layers.

@tailwind base;
@tailwind components;

/* This will always be included in the genereated CSS. And we can use utilities to override it. */
.trix-content {
  /* ... */
}

@tailwind utilities;

Now .trix-content will be included in the generated CSS https://play.tailwindcss.com/WUIZLPgX3M?file=css. It does not belong to any layer.

Note that I also moved @tailwind utilities; to the very bottom. Two reasons:

  1. I don't want to make the decision every time whether this library CSS will be used explicitly in my codebase.
  2. I want to follow the principle that the utility class should always be able to override other classes.

I think this rule is easier to follow for other devs in your team who are not that familiar with Tailwind.

Recap and summary

  • components should be declared before utilitites.
  • @layer allows you to write the CSS anywhere you like, the content will be teleported to the specified layer.
  • CSS defined inside layers are subject to Tree Shaking, they'll be removed in the generated CSS if not used.
  • CSS defined outside layers will always be included in the compiled CSS. Best for third party library CSS.

This is the style I came with up eventually.

@tailwind base;
@tailwind components;

// Base
@layer base {
  @import "base/variables";
  @import "base/elements";
}

// Components
// These styles can be overridden by Utility classes.
// These styles will only be included in the compiled CSS if they are actually used in the files specified in the
// `content` list in tailwind.config.js.
@layer components {
  @import "components/button";
  @import "components/dropdown";
  @import "components/form";
}

// Vendor and other special-purposed CSS
// These styles can be overridden by Utility classes.
// These styles are outside of any @layer so they will always be included in the compiled CSS.
@import "vendor/action_text";
@import "vendor/emoji_picker";
@import "vendor/tribute";

// Other rare cases that you want to preserve the CSS in the complied file.
.some-special-class {
  @apply bg-blue-200 font-normal;
}

// Utilities
// These can override anything come before them.
@tailwind utilities;
@layer utilities {
  @import "utilities/background";
  @import "utilities/link";
}

The Tailwind way of reusing styles

🙏 One last thing to mention. If you use Tailwind following the creators' recommendations, you probably don't need that many "components" than you think you would have in other more "traditional" projects. It's beyond the scope of this post, but I'd highly recommend reading this guide on this topic: https://tailwindcss.com/docs/reusing-styles. For Rails developers, https://viewcomponent.org/ can be your good friend.


Hope you find this article helpful. See you next time 👋

kinopyo avatar
Written By

kinopyo

Indoor enthusiast, web developer, and former hardcore RTS gamer. #parenting
Enjoyed the post?

Clap to support the author, help others find it, and make your opinion count.