Skip to main
a head full of oat milk cappuccinos by
Timo Mämecke
Jump to navigation
· 4 minute read

Style elements across nested layouts with Tailwind

I recently discovered that Tailwind’s group utility is super useful in web frameworks with nested layouts. I only thought of the group utility for smaller UI components, but you actually can use group to style any element based on any other element in the DOM.

This may not seem like a big deal, but consider this example: we hide the header if there’s an element with the CSS class hide-header-signal somewhere in the DOM.

<body class="group/body"> <!-- layout -->
  <header class="group-has-[.hide-header-signal]/body:hidden">...</header>
  <main>
    <h1>Hello world</h1> <!-- page -->
    <div class="hide-header-signal"></div>
  </main>
</body>
html

This scenario is a common layout pattern: you put your header in your layout and render all your pages within this layout. So now all pages share the same header. But this also means that a page cannot change the header. Except with the group utility, we actually can change the header from within the page! Well, to be precise, we can tell the header to change based on what else is in the document. In fact, we can use the named group group/body to change the style of any element based on any other element.

You might think that you could just conditionally render the header to achieve the same thing, but that’s not always easy to do in every web framework.

Here’s a real world example: My blog has a header on all pages, so the header can simply be in the root layout. But I also have two pages for visual regression testing of my syntax highlighting and markdown rendering, and I don’t want to show the header on those pages, because the tests shouldn’t fail when I change the header.1 What other solutions would I have?

  • I don’t have access to the current URL in the root layout, in order to conditionally hide the header based on the URL, unless I would turn the layout into a client component.
  • I could also move the header into every page, instead of having it in the layout. Then I could simply not include it in the pages where I don’t want to show the header. But then I use all advantages of layouts.
  • I could create more nested folders with nested layouts, but I would do it just for those two pages. That’s a significant change for 2 simple test pages.

Although I could do a bunch of things to render the header dynamically for those pages, they all have a comparatively high impact on the codebase. Sometimes this impact is justified and one of these approaches might be a better choice, but clearly not in this case where I just have some unlisted pages for testing purposes.

By using the pattern above with the group utility, it’s now super easy to hide the header on some pages:

// layout.tsx
export default function RootLayout({ children }) {
  return (
    <html>
      <body className="group/body">
        <header className="group-has-[.hide-header-signal]/body:hidden">
          <Header />
        </header>
        {children}
      </body>
    </html>
  )
}

// e.g. regular blog post pages
// posts/[...slug]/page.tsx
export default function Page() {
  return (
    <main>
      <p>The header is visible on this page</p>
    </main>
  )
}

// e.g. pages for visual regression testing
// vrt/[...name]/page.tsx
export default function Page() {
  return (
    <main>
      <p>The header is hidden on this page</p>
      <div className="hide-header-signal" />
    </main>
  )
}
tsx

Done. A simple change which just added a few classes and affected 2 files, instead of changing a lot of code and affecting many files.

This example is only using the has-[.hide-header-signal] modifier, but it’s not limited to only this modifier. You can do even more than just hiding one element based on the existence of another element. For example, you could change the position of an element based on the data attribute of another element. Or change some colors in the layout for some very special pages.

And the nice thing is: it’s just CSS.

…I guess those CSS selectors are slow as hell, but computers are also fast these days.

Footnotes for Section Heading

  1. Footnote 1: Fun fact: I decided to just let those tests fails when the header changes lol. Because when the header changes, then I’m gonna see it anyways. But I still apply this pattern in other places.