Tabs That Become an Accordion: One Component, Two Layouts

Tabs are efficient on wide screens: all section labels are visible at once and one panel shows at a time. On narrow screens they fail — the labels overflow or wrap into a confusing second row — and designs switch to an accordion, where labels stack and each panel opens beneath its own label. Most implementations ship two components and swap them with a media query and JavaScript. The narrow problem this page solves is building one component from native <details> elements that behaves as an accordion when narrow and lays itself out as tabs when wide, with a container query deciding which and no script at all. It belongs to Responsive Component Patterns in the Mastering Container Queries & Responsive Layouts guide.

Why details elements can be tabs

Three platform features make this possible. <details name="…"> groups disclosures so that opening one closes the others, which is exactly the "one panel at a time" behaviour of tabs, with no script. display: contents and grid placement let each <details> element's summary and content be positioned independently in a shared grid — summaries across the top, content below. Container queries switch between the two layouts based on the component's width.

The accessibility model is that of disclosures: each summary is a button that expands its section. That is different from the ARIA tabs pattern, which uses role="tablist" and arrow-key navigation. For content sections — product details, FAQ categories, documentation variants — disclosure semantics are appropriate and arguably simpler for assistive technology users. For application interfaces where users expect arrow keys to move between tabs, the ARIA pattern with script remains the right choice.

Same markup, two arrangements Left, narrow: three summaries stacked vertically, the second open with its panel directly beneath it. Right, wide: three summaries in a row across the top, the second highlighted, and its panel spanning the full width below the row. details[name] as accordion or tabs narrow: accordion Overview Specifications open panel Reviews wide: tabs Overview Specifications Reviews open panel spans the row Opening one closes the others because they share a name attribute.

The complete implementation

Resize the demo: below about 36rem it is an accordion; above, a tab row with the selected panel below.

Live demodetails elements as an accordion or a tab strip
Narrow: an accordion. Drag the frame wider than about 36rem: the same markup becomes a tab row with the open panel below. Only one opens at a time. Drag the bottom-right corner to resize the frame in either direction.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Tabs that become an accordion</title>
<style>
  body { font: 15px/1.5 system-ui, sans-serif; margin: 1rem; }

  .tabset-host { container-type: inline-size; }

  /* ---------- Narrow default: a plain accordion ---------- */
  .tabset details {
    border: 1px solid #cbd5e1;
    border-radius: 8px;
    margin-block-end: 0.5rem;
  }
  .tabset summary {
    padding: 0.6rem 0.9rem;
    cursor: pointer;
    font-weight: 600;
  }
  .tabset summary:focus-visible { outline: 2px solid #4338ca; outline-offset: 2px; }
  .tabset .panel { padding: 0 0.9rem 0.9rem; }

  /* ---------- Wide: a tab strip ---------- */
  @container (width > 36rem) {
    .tabset {
      display: grid;
      /* One auto column per tab, then the panel row spans them all. */
      grid-template-columns: repeat(3, auto) 1fr;
      grid-template-rows: auto 1fr;
    }

    /* Let each details' children join the tabset grid directly. */
    .tabset details { display: contents; }

    .tabset summary {
      grid-row: 1;
      border: 1px solid #cbd5e1;
      border-block-end: 0;
      border-radius: 8px 8px 0 0;
      list-style: none;                 /* hide the disclosure triangle */
    }
    .tabset summary::-webkit-details-marker { display: none; }

    .tabset details[open] > summary {
      background: #eef2ff;
      color: #312e81;
    }

    /* With details as display: contents, the grid item for the content
       is the ::details-content box that wraps .panel, so place that. */
    .tabset details::details-content {
      grid-row: 2;
      grid-column: 1 / -1;              /* span the whole strip */
    }
    .tabset .panel {
      padding: 1rem;
      border: 1px solid #cbd5e1;
      border-radius: 0 8px 8px 8px;
    }
  }
</style>
</head>
<body>
  <div class="tabset-host">
    <div class="tabset">
      <details name="product" open>
        <summary>Overview</summary>
        <div class="panel"><p>A lightweight two-person tent for three seasons.</p></div>
      </details>
      <details name="product">
        <summary>Specifications</summary>
        <div class="panel"><p>1.4 kg · packed 42 × 14 cm · 2 doors.</p></div>
      </details>
      <details name="product">
        <summary>Reviews</summary>
        <div class="panel"><p>"Pitches in five minutes, even in wind."</p></div>
      </details>
    </div>
  </div>
</body>
</html>

display: contents on each <details> is what lets the summaries line up in a single row: the <details> boxes disappear from layout, and what they contain becomes direct grid items of .tabset. For a <details> element that means two boxes: the <summary>, and the ::details-content pseudo-element that wraps everything else. Summaries go in row 1, each in its own auto-sized column in source order; the ::details-content boxes go in row 2 spanning every column. A closed <details> hides its ::details-content natively, so only the open panel occupies row 2. Placing .panel directly would not work, because its parent box is the ::details-content wrapper, not the grid.

The key technique: exclusivity from the name attribute

Tabs require that exactly one panel is open. Shared name attributes enforce the "at most one" half: opening any member of the group closes the others, just as selecting a radio button deselects its siblings. Starting with one element open provides the "at least one" half on load.

Exclusive disclosures Before: the first of three details elements named product is open. The user activates the third summary. After: the third is open and the browser has closed the first automatically. No script is involved. <details name="product"> × 3 before Overview · open Specifications Reviews open Reviews after Overview · closed Specifications Reviews · open Unlike tabs, clicking the open summary closes it; nothing forces one to stay open.

One behavioural difference from true tabs remains: activating the summary of the open panel closes it, leaving no panel visible. In the accordion layout that is expected. In the tab layout it can feel odd; a small script can re-open it, or the design can accept it — an empty panel area with all labels visible is a clear, recoverable state.

When the labels no longer fit

The tab layout assumes all summaries fit on one row. With three short labels that is safe above 36rem; with six labels, or labels translated into a longer language, it may not be. Three options keep the component robust as content changes.

Raise the threshold with the count. A quantity query can require more width before switching to tabs when there are more sections: the tab rules for five or more <details> elements sit inside a wider @container condition.

Let the strip scroll. Make row 1 a horizontally scrolling strip by placing the summaries in their own grid columns inside an overflow-x: auto region. Keyboard users can still Tab through them, and the active tab scrolls into view when focused. This suits product pages with many specification groups.

Fall back to the accordion earlier than necessary. An accordion never runs out of room. If the labels are long and unpredictable, a conservative threshold that switches to tabs only in genuinely wide containers is simpler than engineering a strip that copes with every case.

Whichever option is chosen, test with the longest label the content will realistically have, and at 200% text zoom — where a threshold in rem rises with the text and the component returns to the accordion before the labels collide.

Accessibility considerations

  • Semantics. Screen readers announce each summary as a button with expanded or collapsed state, in both layouts. Users navigating by heading will not find the section labels unless summaries contain headings, so put an <h3> inside each <summary> if the sections are substantial.
  • Keyboard. Tab moves between summaries; Enter or Space toggles. There is no arrow-key navigation between tabs, which users of the ARIA tabs pattern expect; document the component's behaviour in your design system so it is chosen knowingly.
  • Find in page. Closed <details> contents are searchable in current browsers, and a match opens its disclosure automatically — an advantage over many scripted tab implementations that hide inactive panels with display: none.
  • Visual state. The open tab's highlighted summary uses both colour and shape (it joins the panel), so the state is not conveyed by colour alone.

Variation: animating the panel

In the accordion layout, panels can grow to their height smoothly with interpolate-size and ::details-content, described in Animating height: auto With interpolate-size. In the tab layout, a short fade is usually better than a height animation, because the panel area stays roughly the same size.

@container (width > 36rem) {
  @media (prefers-reduced-motion: no-preference) {
    .tabset details[open] > .panel {
      animation: tab-in 180ms ease-out;
    }
  }
}

@keyframes tab-in {
  from { opacity: 0; translate: 0 0.25rem; }
}

The animation plays each time a panel opens, because the panel element is rendered afresh when its <details> opens. Keep it short: users switching tabs rapidly to compare content should not wait for each panel to arrive.

Browser support

The name attribute for exclusive <details> groups is supported in current versions of Chrome, Edge, Firefox and Safari; in older engines, multiple disclosures can be open at once, which degrades the tab layout to several visible panels but loses no content. The ::details-content pseudo-element, which the tab layout places in the grid, is supported in Chrome and Edge 131+, Firefox 143+ and Safari 18.4+; where it is missing, keep the accordion layout by wrapping the wide rules in @supports selector(::details-content). Container queries are supported in Chrome and Edge 105+, Firefox 110+ and Safari 16+. display: contents on <details> behaves consistently in current engines for layout purposes; test with your screen readers, as some older combinations mishandled the semantics of elements with display: contents.

FAQ

Can the same markup be both tabs and an accordion? Yes, if it is built from details elements that share a name attribute. The name makes them mutually exclusive, like tabs. In narrow containers they display as a normal stacked accordion; in wide containers CSS grid places every summary in a row at the top and the open panel below.

Is a details-based tab strip accessible? It is accessible as a set of disclosures: each summary is announced as a button that expands and collapses, and keyboard users Tab between them. It is not announced with the ARIA tabs pattern of tablist, tab and tabpanel. For many content sections that is acceptable; for application-style tabs with arrow-key navigation, use the ARIA pattern with script.

What does the name attribute on details do? It groups details elements into an exclusive set: opening one closes any other open element with the same name. It behaves like radio buttons for disclosures and needs no script.

Why use a container query rather than a media query for this switch? Tab strips need horizontal room for all their labels. The same component may sit in a wide main column or a narrow sidebar, and only a container query knows how much room it actually has.

Related articles

More pages in the same section.