Monday, October 27, 2025

Pure CSS Tabs With Particulars, Grid, and Subgrid


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

ingredient together with CSS Grid and Subgrid.

First, the HTML

Let’s begin by organising the HTML construction. We are going to want a set of

parts inside a dad or mum wrapper that we’ll name .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

parts — up as a grid and set their columns and rows to inherit the primary .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

ingredient takes up the whole accessible house horizontally and vertically utilizing 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

parts and conceal 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

ingredient accommodates a

offering each the tab label and button that toggles each open and closed.

Let’s place the

ingredient within the first subgrid row and add apply mild styling when a

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

parts within the subgrid’s columns so they’re not blocking one another. We’ll use 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

ingredient. I prefer to inline the variables immediately in HTML and use them as hooks for styling:

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

ingredient as a result of all the

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

parts on prime by setting z-index.

abstract {
  z-index: 1;
}

Right here’s the total working demo:

Accessibility

The

ingredient contains built-in accessibility options, akin to keyboard navigation and display reader help, for each expanded and collapsed states. I’m positive we may make it even higher, nevertheless it could be a subject for an additional article. I’d love some suggestions within the feedback to assist cowl as many bases as attainable.

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.

Related Articles

Latest Articles