
This is a pure CSS tabs component that creates animated tabbed content using the <details> and <summary> tags combined with CSS display:contents.
This tabs component supports keyboard navigation, includes smooth animations, and adapts responsively to different screen sizes. The tabs also feature a sliding indicator that highlights the active tab and animated content transitions when switching between tabbed content.
Use Cases:
- Content Organization: Organize large amounts of content into manageable sections. Users can easily navigate through different tabs to find the information they need.
- Interactive Galleries: Create interactive image or video galleries where users can switch between different media items by clicking on tabs.
- Comparison Tools: Develop comparison tools that allow users to compare different products, services, or data sets side by side.
How to use it:
1. Build the HTML structure for the tabs and their corresponding content. Each tab consists of a <details> element. The tab title appears within the <summary> tag, and the content resides within a <div> with the class “tab-contents”.
<div class="tabs">
<details name="tab" tabindex="1" open>
<summary>Tab 1</summary>
<div class="tab-contents-wrapper">
<div class="tab-contents">
Tabbed Content 1
</div>
</div>
</details>
<details name="tab" tabindex="2">
<summary>Tab 2</summary>
<div class="tab-contents-wrapper">
<div class="tab-contents">
Tabbed Content 2
</div>
</div>
</details>
<details name="tab" tabindex="3">
<summary>Tab 3</summary>
<div class="tab-contents-wrapper">
<div class="tab-contents">
Tabbed Content 3
</div>
</div>
</details>
... more tabs here ...
</div>2. Add the Core CSS:
:root {
--clr-bg: #222;
--clr-txt: #111;
--clr-primary: rgb(248, 250, 252);
--clr-secondary: rgb(56, 189, 248);
}
.tabs {
--tabs-line-clr: var(--clr-primary);
--tabs-line-thickness: 1px;
--tabs-height: 30px;
--tab-width: 100px;
--tab-font-size: 1rem;
--tab-clr: var(--clr-primary);
--tab-clr-hover: var(--clr-secondary);
--tab-indicator-height: 5px;
--tab-indicator-clr: var(--clr-secondary);
position: relative;
width: min(calc(100% - 1rem), 600px);
display: flex;
flex-wrap: wrap;
align-content: start;
gap: 0rem;
}
@media (max-width: 600px){
.tabs{
--tab-width: 70px;
--tab-font-size: .8rem;
}
}
/* line under tabs */
.tabs::before {
content: "";
position: absolute;
width: 100%;
height: var(--tabs-line-thickness);
top: var(--tabs-height);
background-color: var(--tabs-line-clr);
}
/* current tab indicator */
.tabs::after {
content: "";
position: absolute;
top: calc(var(--tabs-height) - var(--tab-indicator-height) / 2);
left: 0;
width: var(--tab-indicator-width,0px);
height: var(--tab-indicator-height);
border-radius: 99px;
transition: translate 300ms ease-in-out, width 300ms ease-in-out,
opacity 300ms ease-in-out;
background-color: var(--tab-indicator-clr);
translate: var(--tab-indicator-x);
opacity: var(--tab-indicator-opacity, 1);
}
/* focus visible and open */
/* If we could use a counter within the calulations that way it would not be necesarry to define each one manually */
.tabs:has(details:nth-child(1) > summary:focus-visible),
.tabs:has(details[open]:nth-child(1)) {
--tab-indicator-x: calc(var(--tab-width) * 0);
--tab-indicator-width: var(--tab-width);
}
.tabs:has(details:nth-child(2) > summary:focus-visible),
.tabs:has(details[open]:nth-child(2)) {
--tab-indicator-x: calc(var(--tab-width) * 1);
--tab-indicator-width: var(--tab-width);
}
.tabs:has(details:nth-child(3) > summary:focus-visible),
.tabs:has(details[open]:nth-child(3)) {
--tab-indicator-x: calc(var(--tab-width) * 2);
--tab-indicator-width: var(--tab-width);
}
.tabs:has(details:nth-child(4) > summary:focus-visible),
.tabs:has(details[open]:nth-child(4)) {
--tab-indicator-x: calc(var(--tab-width) * 3);
--tab-indicator-width: var(--tab-width);
}3. Style the Tab Headers:
.tabs:has(details:not([open]):nth-child(1) > summary:focus-visible),
.tabs:has(details:not([open]):nth-child(2) > summary:focus-visible),
.tabs:has(details:not([open]):nth-child(3) > summary:focus-visible),
.tabs:has(details:not([open]):nth-child(4) > summary:focus-visible) {
--tab-indicator-opacity: 0.5;
}
.tabs > details {
display: contents; /* this is the key! */
}
.tabs > details[open] {
--tab-contents-grid-rows: 1fr;
}
.tabs > details > summary {
font-size: var(--tab-font-size);
border: none;
outline: none;
cursor: pointer;
display: block;
order: 0;
height: var(--tab-height);
width: var(--tab-width);
color: var(--tab-clr);
display: grid;
place-content: center;
list-style: none;
transition: color 300ms ease-in-out;
}
.tabs > details > summary::-webkit-details-marker {
/* Hides marker on Safari */
display: none;
}
.tabs > details > summary:focus-visible,
.tabs > details > summary:hover {
color: var(--tab-clr-hover);
}
.tabs > details > .tab-contents-wrapper {
order: 1;
width: 100%;
display: grid;
grid-template-rows: var(--tab-contents-grid-rows, 0fr);
margin-top: 1rem;
transition: grid-template-rows 300ms ease-in-out;
}
.tabs > details .tab-contents {
overflow: hidden; /* for grid-template-rows animation */
}4. Create smooth animations:
.tabs > details[open] .tab-contents > * {
animation: slide-up 1000ms;
}
@keyframes slide-up {
from {
opacity: 0;
translate: 0 30px;
}
to {
opacity: 1;
translate: 0;
}
}How It Works:
The tabs component uses CSS display:contents on the <details> elements to flatten the DOM structure while maintaining semantic markup. The active tab indicator moves using CSS custom properties and the :has selector. Content animations use grid-template-rows transitions for smooth height changes.
The tabs respond to both click and keyboard interactions. When a tab gains focus or opens, CSS updates the indicator position through the --tab-indicator-x variable. Content transitions use a combination of grid layout and opacity animations for a better user experience.







