Pure CSS tabs with details, grid and sub-grid | CSS tricks

Pure CSS tabs with details, grid and sub-grid | CSS tricks

5 minutes, 26 seconds Read

Creating a tab interface with CSS is an endless topic in the world of modern web development. Are they possible? If so, could these be accessible? I’ve written about how to build them the first time nine long years ago, and how integrate accessible practices in them.

Although my solution at the time could do that possible are still being applied, I’ve landed on a more modern approach to CSS tabs using the

element in combination with CSS Grid and Subgrid.

First, the HTML

Let’s start by setting up the HTML structure. We need a set

elements in a parent wrapper that we will call .grid. Each

will be a .item as you might imagine, each tab is a tab in the interface.

First item
Second item
Third item

These don’t look like real tabs yet! But it’s the right structure we want before we get into CSS, where we’ll put CSS Grid and Subgrid to work.

Then the CSS

Let’s set the grid for our wrapper element using (you guessed it) CSS grid. What we’re actually creating is a three-column grid, one column for each tab (or .item), with a little space in between.

We will also set two rows in the .gridone that is tailored to the content and one that is proportionate to the available space. The first row contains our tabs and the second row is reserved for displaying the active tab panel.

.grid {
  display: grid;
  grid-template-columns: repeat(3, minmax(200px, 1fr));
  grid-template-rows: auto 1fr;
  column-gap: 1rem;
}

Now we look a little more tab-like:

Next we need to set up the subgrid for our tab elements. We want a subgrid because it allows us to use the existing one .grid lines without nesting an entirely new grid of new lines. This way everything fits together nicely.

So we set each tab: the

elements – as a grid and set their columns and rows to inherit the main elements .grid‘s lines with subgrid.

details {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
}

Additionally, we want each tab element to fill the whole .gridso we set it up so that the

element takes up the entire available space horizontally and vertically using the grid-column And grid-row properties:

details {
  display: grid;
  grid-template-columns: subgrid;
  grid-template-rows: subgrid;
  grid-column: 1 / -1;
  grid-row: 1 / span 3;
}

It looks a little weird at first because the three tabs are stacked right on top of each other, but they cover the entire surface. .grid that’s exactly what we want.

Next, we place the contents of the tab panel in the second row of the subgrid and stretch it across all three columns. We use ::details-content (good support, but not yet in WebKit at the time of writing) to target the panel contents, which is nice because it means we don’t have to set another wrapper in the markup just for that purpose.

details::details-content {
  grid-row: 2; /* position in the second row */
  grid-column: 1 / -1; /* cover all three columns */
  padding: 1rem;
  border-bottom: 2px solid dodgerblue;
}

The problem with a tabbed interface is that we only want to show one open tab panel at a time. Fortunately, we can [open] state of the

elements and hide the ::details-content from any tab :not([open])by using enable selectors:

details:not([open])::details-content {
  display: none;
}

We still have overlapping tabs, but the only tab panel we show is currently open, which cleans things up quite a bit:

Turn

in tabs

Now on to the fun stuff! At this point, all of our tabs are visually stacked. We want to spread it out and distribute it evenly throughout the world .grid‘s top row. Each

element contains a

by providing both the tab label and the button to open and close each tab.

Let’s get the

element in the first subgrid row and add light styling when a

tab is located in a [open] stands:

summary {
  grid-row: 1; /* First subgrid row */
  display: grid;
  padding: 1rem; /* Some breathing room */
  border-bottom: 2px solid dodgerblue;
  cursor: pointer; /* Update the cursor when hovered */
}

/* Style the  element when 
is [open] */ details[open] summary { font-weight: bold; }

Our tabs are still stacked, but how we’ve applied some light styles when a tab is open:

We’re almost there! The last thing you need to do is position the

elements in the columns of the subgrid so that they no longer block each other. We use the :nth-of-type pseudo to select them all individually in order in the HTML:

/* First item in first column */
details:nth-of-type(1) summary {
  grid-column: 1 / span 1;
}
/* Second item in second column */
details:nth-of-type(2) summary {
  grid-column: 2 / span 1;
}
/* Third item in third column */
details:nth-of-type(3) summary {
  grid-column: 3 / span 1;
}

Look at that! The tabs are evenly spaced across the top row of the subgrid:

Unfortunately we can’t use loops in CSS (yet), but we can use variables to keep our styles DRY:

summary {
  grid-column: var(--n) / span 1;
}

Now we have to do the --n variable for each

element. I like to put the variables directly into HTML and use them as hooks for styling:

First item
Second item
Third item

Again, since loops are not an issue in CSS at this point, I tend to reach for a templating language specifically Liquidto get some looping action. This way there is no need to explicitly write the HTML for each tab:

{% for item in itemList %}
  
{% endfor %}

You can of course roll with a different template language. There are plenty if you like to keep things concise!

Final touches

Okay, I lied. There is one more thing we should do. At the moment you can only click on the latter

element because all

pieces are stacked on top of each other in a manner where the last one is on top of the stack.

You may have guessed it already: we have to…

elements at the top by setting z-index.

summary {
  z-index: 1;
}

Here’s the full working demo:

Accessibility

The

element includes built-in accessibility features, such as keyboard navigation and screen reader support, for both expanded and collapsed states. I’m sure we can make it even better, but it might be a topic for another article. I’d love some feedback in the comments to help cover as many bases as possible.

Update: Nathan Knowler sounded in with some excellent points on Mastodon. Adrian Roselli entered with additional context in the comments as well.


It’s 2025 and we can only create tabs with HTML and CSS, without any hacking. I don’t know about you, but this developer is happy today, even if we still need some patience before browsers fully support these features.

#Pure #CSS #tabs #details #grid #subgrid #CSS #tricks

Similar Posts

Leave a Reply

Your email address will not be published. Required fields are marked *