{"id":389805,"date":"2025-10-27T08:33:17","date_gmt":"2025-10-27T14:33:17","guid":{"rendered":"https:\/\/css-tricks.com\/?p=389805"},"modified":"2025-10-27T10:19:39","modified_gmt":"2025-10-27T16:19:39","slug":"pure-css-tabs-with-details-grid-and-subgrid","status":"publish","type":"post","link":"https:\/\/css-tricks.com\/pure-css-tabs-with-details-grid-and-subgrid\/","title":{"rendered":"Pure CSS Tabs With Details, Grid, and Subgrid"},"content":{"rendered":"\n

Making a tab interface with CSS is a never-ending topic in the world of modern web development. Are they possible? If yes, could they be accessible? I wrote how to build them the first time<\/a> nine long years ago, and how to<\/a> integrate accessible practices<\/a> into them.<\/p>\n\n\n\n

Although my solution then could possibly<\/em> still be applied today, I\u2019ve landed on a more modern approach to CSS tabs using the <details><\/code><\/a> element in combination with CSS Grid<\/a> and Subgrid<\/a>.<\/p>\n\n\n\n\n\n\n\n

First, the HTML<\/h3>\n\n\n

Let\u2019s start by setting up the HTML structure. We will need a set of <details><\/code> elements inside a parent wrapper that we\u2019ll call .grid<\/code>. Each <details><\/code> will be an .item<\/code> as you might imagine each one being a tab in the interface.<\/p>\n\n\n\n

<div class=\"grid\">\n  <!-- First tab: set to open -->\n  <details class=\"item\" name=\"alpha\" open>\n    <summary class=\"subitem\">First item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n  <details class=\"item\" name=\"alpha\">\n    <summary class=\"subitem\">Second item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n  <details class=\"item\" name=\"alpha\">\n    <summary class=\"subitem\">Third item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n<\/div><\/code><\/pre>\n\n\n\n
\"\"<\/figure>\n\n\n\n

These don\u2019t look like true tabs yet! But it\u2019s the right structure we want before we get into CSS, where we\u2019ll put CSS Grid and Subgrid to work.<\/p>\n\n\n

Next, the CSS<\/h3>\n\n\n

Let\u2019s set up the grid for our wrapper element using \u2014 you guessed it \u2014 CSS Grid. Basically what we\u2019re making is a three-column grid, one column for each tab (or .item<\/code>), with a bit of spacing between them.<\/p>\n\n\n\n

We\u2019ll also set up two rows in the .grid<\/code>, one that\u2019s sized to the content and one that maintains its proportion with the available space. The first row will hold our tabs and the second row is reserved for the displaying the active tab panel.<\/p>\n\n\n\n

.grid {\n  display: grid;\n  grid-template-columns: repeat(3, minmax(200px, 1fr));\n  grid-template-rows: auto 1fr;\n  column-gap: 1rem;\n}<\/code><\/pre>\n\n\n\n

Now we\u2019re looking a little more tab-like:<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n

Next, we need to set up the subgrid for our tab elements.<\/strong> We want subgrid because it allows us to use the existing .grid<\/code> lines without nesting an entirely new grid with new lines. Everything aligns nicely this way.<\/p>\n\n\n\n

So, we\u2019ll set each tab \u2014 the <details><\/code> elements \u2014 up as a grid and set their columns and rows to inherit the main .grid<\/code>‘s lines with subgrid<\/code>.<\/p>\n\n\n\n

details {\n  display: grid;\n  grid-template-columns: subgrid;\n  grid-template-rows: subgrid;\n}<\/code><\/pre>\n\n\n\n

Additionally, we want each tab element to fill the entire .grid<\/code>, so we set it up so that the <details><\/code> element takes up the entire available space horizontally and vertically using the grid-column<\/code> and grid-row<\/code> properties:<\/p>\n\n\n\n

details {\n  display: grid;\n  grid-template-columns: subgrid;\n  grid-template-rows: subgrid;\n  grid-column: 1 \/ -1;\n  grid-row: 1 \/ span 3;\n}<\/code><\/pre>\n\n\n\n

It looks a little wonky at first because the three tabs are stacked right on top of each other, but they cover the entire .grid<\/code> which is exactly what we want.<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n

Next, we will place the tab panel content in the second row of the subgrid and stretch it across all three columns. We\u2019re using ::details-content<\/code><\/a> (good support, but not yet in WebKit<\/a> at the time of writing) to target the panel content, which is nice because that means we don\u2019t need to set up another wrapper in the markup simply for that purpose.<\/p>\n\n\n\n

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

The thing about a tabbed interface is that we only want to show one open tab panel at a time. Thankfully, we can select the [open]<\/code> state of the <details><\/code> elements and hide the ::details-content<\/code> of any tab that is :not([open])<\/code>by using enabling selectors<\/a>:<\/p>\n\n\n\n

details:not([open])::details-content {\n  display: none;\n}<\/code><\/pre>\n\n\n\n

We still have overlapping tabs, but the only tab panel we\u2019re displaying is currently open, which cleans things up quite a bit:<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n

Turning <details><\/code> into tabs<\/h3>\n\n\n

Now on to the fun stuff! Right now, all of our tabs are visually stacked. We want to spread those out and distribute them evenly along the .grid<\/code>‘s top row. Each <details><\/code> element contains a <summary><\/code> providing both the tab label and button that toggles each one open and closed.<\/p>\n\n\n\n

Let\u2019s place the <summary><\/code> element in the first subgrid row and add apply light styling when a <details><\/code> tab is in an [open]<\/code> state:<\/p>\n\n\n\n

summary {\n  grid-row: 1; \/* First subgrid row *\/\n  display: grid;\n  padding: 1rem; \/* Some breathing room *\/\n  border-bottom: 2px solid dodgerblue;\n  cursor: pointer; \/* Update the cursor when hovered *\/\n}\n\n\/* Style the <summary> element when <details> is [open] *\/\ndetails[open] summary {\n  font-weight: bold;\n}<\/code><\/pre>\n\n\n\n

Our tabs are still stacked, but how we have some light styles applied when a tab is open:<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n

We\u2019re almost there! The last thing is to position the <summary><\/code> elements in the subgrid\u2019s columns so they are no longer blocking each other. We\u2019ll use the :nth-of-type<\/code> pseudo to select each one individually by their order in the HTML:<\/p>\n\n\n\n

\/* First item in first column *\/\ndetails:nth-of-type(1) summary {\n  grid-column: 1 \/ span 1;\n}\n\/* Second item in second column *\/\ndetails:nth-of-type(2) summary {\n  grid-column: 2 \/ span 1;\n}\n\/* Third item in third column *\/\ndetails:nth-of-type(3) summary {\n  grid-column: 3 \/ span 1;\n}<\/code><\/pre>\n\n\n\n

Check that out! The tabs are evenly distributed along the subgrid\u2019s top row:<\/p>\n\n\n\n

\"\"<\/figure>\n\n\n\n

Unfortunately, we can\u2019t use loops in CSS (yet!), but we can use variables to keep our styles DRY:<\/p>\n\n\n\n

summary {\n  grid-column: var(--n) \/ span 1;\n}<\/code><\/pre>\n\n\n\n

Now we need to set the --n<\/code> variable for each <details><\/code> element. I like to inline the variables directly in HTML and use them as hooks for styling:<\/p>\n\n\n\n

<div class=\"grid\">\n  <details class=\"item\" name=\"alpha\" open style=\"--n: 1\">\n    <summary class=\"subitem\">First item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n  <details class=\"item\" name=\"alpha\" style=\"--n: 2\">\n    <summary class=\"subitem\">Second item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n  <details class=\"item\" name=\"alpha\" style=\"--n: 3\">\n    <summary class=\"subitem\">Third item<\/summary>\n    <div><!-- etc. --><\/div>\n  <\/details>\n<\/div><\/code><\/pre>\n\n\n\n

Again, because loops aren\u2019t a thing in CSS at the moment, I tend to reach for a templating language, specifically Liquid<\/a>, to get some looping action. This way, there\u2019s no need to explicitly write the HTML for each tab:<\/p>\n\n\n\n

{% for item in itemList %}\n  <div class=\"grid\">\n    <details class=\"item\" name=\"alpha\" style=\"--n: {{ forloop.index }}\" {% if forloop.first %}open{% endif %}>\n      <!-- etc. -->\n    <\/details>\n  <\/div>\n{% endfor %}<\/code><\/pre>\n\n\n\n

You can roll with a different templating language, of course. There are plenty out there if you like keeping things concise!<\/p>\n\n\n

Final touches<\/h3>\n\n\n

OK, I lied. There\u2019s one more thing<\/em> we ought to do. Right now, you can click only on the last <summary><\/code> element because all of the <details><\/code> pieces are stacked on top of each other in a way where the last one is on top of the stack.<\/p>\n\n\n\n

You might have already guessed it: we need to put our <summary><\/code> elements on top by setting z-index<\/a><\/code>.<\/p>\n\n\n\n

summary {\n  z-index: 1;\n}<\/code><\/pre>\n\n\n\n

Here\u2019s the full working demo:<\/p>\n\n\n\n