Create Accessible Tabs with Pure CSS and Semantic HTML

Category: CSS & CSS3 | November 19, 2024
Authorcbolson
Last UpdateNovember 19, 2024
LicenseMIT
Tags
Views162 views
Create Accessible Tabs with Pure CSS and Semantic HTML

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.

You Might Be Interested In:


Leave a Reply