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
First, the HTML
Let’s start by setting up the HTML structure. We need a set
.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
.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
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
::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
by providing both the tab label and the button to open and close each tab.Let’s get the
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
: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
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
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…
z-index.summary {
z-index: 1;
}Here’s the full working demo:
Accessibility
The
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


