Making a tab interface with CSS is a unending matter on the planet of contemporary net growth. Are they attainable? If sure, may they be accessible? I wrote the best way to construct them the primary time 9 lengthy years in the past, and the best way to combine accessible practices into them.
Though my answer then may probably nonetheless be utilized at present, I’ve landed on a extra fashionable method to CSS tabs utilizing the
First, the HTML
Let’s begin by organising the HTML construction. We are going to want a set of
.grid. Every
might be an .merchandise as you may think each being a tab within the interface.
First merchandise
Second merchandise
Third merchandise
These don’t appear like true tabs but! But it surely’s the best construction we wish earlier than we get into CSS, the place we’ll put CSS Grid and Subgrid to work.
Subsequent, the CSS
Let’s arrange the grid for our wrapper ingredient utilizing — you guessed it — CSS Grid. Principally what we’re making is a three-column grid, one column for every tab (or .merchandise), with a little bit of spacing between them.
We’ll additionally arrange two rows within the .grid, one which’s sized to the content material and one which maintains its proportion with the accessible house. The primary row will maintain our tabs and the second row is reserved for the airing the lively tab panel.
.grid {
show: grid;
grid-template-columns: repeat(3, minmax(200px, 1fr));
grid-template-rows: auto 1fr;
column-gap: 1rem;
}
Now we’re trying a bit of extra tab-like:

Subsequent, we have to arrange the subgrid for our tab parts. We wish subgrid as a result of it permits us to make use of the present .grid traces with out nesting a completely new grid with new traces. Every little thing aligns properly this fashion.
So, we’ll set every tab — the
.grid‘s traces with subgrid.
particulars {
show: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
}
Moreover, we wish every tab ingredient to fill the whole .grid, so we set it up in order that the
grid-column and grid-row properties:
particulars {
show: grid;
grid-template-columns: subgrid;
grid-template-rows: subgrid;
grid-column: 1 / -1;
grid-row: 1 / span 3;
}
It appears a bit of wonky at first as a result of the three tabs are stacked proper on prime of one another, however they cowl the whole .grid which is precisely what we wish.

Subsequent, we’ll place the tab panel content material within the second row of the subgrid and stretch it throughout all three columns. We’re utilizing ::details-content (good help, however not but in WebKit on the time of writing) to focus on the panel content material, which is sweet as a result of which means we don’t have to arrange one other wrapper within the markup merely for that objective.
particulars::details-content {
grid-row: 2; /* place within the second row */
grid-column: 1 / -1; /* cowl all three columns */
padding: 1rem;
border-bottom: 2px stable dodgerblue;
}
The factor a couple of tabbed interface is that we solely need to present one open tab panel at a time. Fortunately, we are able to choose the [open] state of the
::details-content of any tab that’s :not([open])through the use of enabling selectors:
particulars:not([open])::details-content {
show: none;
}
We nonetheless have overlapping tabs, however the one tab panel we’re displaying is presently open, which cleans issues up fairly a bit:

Turning
into tabs
Now on to the enjoyable stuff! Proper now, all of our tabs are visually stacked. We need to unfold these out and distribute them evenly alongside the .grid‘s prime row. Every
offering each the tab label and button that toggles each open and closed.
Let’s place the
tab is in an [open] state:
abstract {
grid-row: 1; /* First subgrid row */
show: grid;
padding: 1rem; /* Some respiration room */
border-bottom: 2px stable dodgerblue;
cursor: pointer; /* Replace the cursor when hovered */
}
/* Model the ingredient when is [open] */
particulars[open] abstract {
font-weight: daring;
}
Our tabs are nonetheless stacked, however how we’ve some mild kinds utilized when a tab is open:

We’re virtually there! The very last thing is to place the
:nth-of-type pseudo to pick each individually by their order within the HTML:
/* First merchandise in first column */
particulars:nth-of-type(1) abstract {
grid-column: 1 / span 1;
}
/* Second merchandise in second column */
particulars:nth-of-type(2) abstract {
grid-column: 2 / span 1;
}
/* Third merchandise in third column */
particulars:nth-of-type(3) abstract {
grid-column: 3 / span 1;
}
Verify that out! The tabs are evenly distributed alongside the subgrid’s prime row:

Sadly, we are able to’t use loops in CSS (but!), however we are able to use variables to maintain our kinds DRY:
abstract {
grid-column: var(--n) / span 1;
}
Now we have to set the --n variable for every
First merchandise
Second merchandise
Third merchandise
Once more, as a result of loops aren’t a factor in CSS in the intervening time, I have a tendency to succeed in for a templating language, particularly Liquid, to get some looping motion. This manner, there’s no have to explicitly write the HTML for every tab:
{% for merchandise in itemList %}
{% endfor %}
You may roll with a unique templating language, in fact. There are lots on the market in case you like retaining issues concise!
Last touches
OK, I lied. There’s yet one more factor we must do. Proper now, you’ll be able to click on solely on the final
items are stacked on prime of one another in a manner the place the final one is on prime of the stack.
You might need already guessed it: we have to put our
z-index.
abstract {
z-index: 1;
}
Right here’s the total working demo:
Accessibility
The
Replace: Nathan Knowler chimed in with some wonderful factors over on Mastodon. Adrian Roselli buzzed in with extra context within the feedback as properly.
It’s 2025, and we are able to create tabs with HTML and CSS solely with none hacks. I don’t learn about you, however this developer is joyful at present, even when we nonetheless want a bit of persistence for browsers to completely help these options.
