How to build an accordion with HTML and CSS without JavaScript: the details element
An accordion is built on the details element: the browser opens the items, and the name attribute keeps only one open. We show the code, a custom arrow, smooth opening in CSS and browser support.
An accordion in HTML and CSS needs no JavaScript: it is built on the details element. The item heading goes into summary, the hidden text follows it, and the browser opens the item itself. To keep only one item open at a time, we give every details of the group the same name attribute (Chrome and Edge 120, Firefox 130, Safari 17.2). Smooth opening is added by CSS with ::details-content, but only Chrome and Edge can animate the auto height so far: in other browsers the item opens at once.
In short
- An accordion item is a
detailswhose first child issummary. A click onsummaryopens and closes the item, no script needed. - An item is open while
detailshas theopenattribute. It does not touch other items. - The same
nameon severaldetailsjoins them into a group: opening one closes another. The group does not require one item to be open all the time: a click on the open item closes it. - We remove the standard arrow with
list-style: noneand::-webkit-details-marker, and draw our own in::afteronsummary. - An item works from the keyboard: Tab moves to
summary, Enter and Space open and close it. We checked this in Chrome 154. - Smooth opening comes from
::details-content(Chrome and Edge 131, Firefox 143, Safari 18.4) together withinterpolate-size, which only Chrome and Edge understand. Without it the accordion works the same, only without animation.
How to build an accordion on details
An accordion is a list of items, each with a heading, and the text under it stays hidden until the item is open. HTML already has such an item: the details element hides everything except summary and shows it after a click on the heading.
<details>
<summary>What is an accordion?</summary>
<p>A list of items, each with a heading and hidden text.</p>
</details>
<details>
<summary>Do we need JavaScript?</summary>
<p>No, the browser opens the items itself.</p>
</details>
We put summary first inside details. Without it the browser shows its own label, usually "Details". Everything else inside details is ordinary markup: paragraphs, lists, links, forms.
An item is open while details has the boolean attribute open. To make an item open on page load we write <details open>. The browser adds and removes this attribute itself when the user clicks the heading. To close an item in the markup we remove the attribute entirely: open="false" opens the item, because the value of a boolean attribute is not read.
Every switch is reported by the toggle event. It arrives after the state has changed, and if the state changed several times before dispatch, the events are merged into one. A plain HTML accordion does not need the event; it is useful when our code has to react to an item opening, for example to load data.
The example below shows two sets of three items. The set titled «Без name» (without name) consists of ordinary details, and the set titled «С name» (with name) gets a group, which the next section covers. Let us open all three items in the first set: they stay open at the same time. If the sets stand one under the other on a narrow screen, the behavior does not change. The result is easy to check: we open the second item in the set «С name», and the first one closes by itself. The live examples are shared with the Russian version of the article, so the text inside them is in Russian.
How to keep only one item open
For an accordion with one open item we give every details of the group the same name.
<details name="faq" open>
<summary>What is an accordion?</summary>
<p>A list of items, each with a heading and hidden text.</p>
</details>
<details name="faq">
<summary>Do we need JavaScript?</summary>
<p>No, the browser opens the items itself.</p>
</details>
<details name="faq">
<summary>Can two items be open at once?</summary>
<p>No, opening an item closes the previous one.</p>
</details>
The browser keeps track of the group itself. In the set «С name» of the previous example we opened the items one after another, and in Chrome 154 only the last one stayed open. If we then click an item that is already open, it closes, and the group has no open items: name limits the number of open items from above, but does not require that one of them is always open.
A few details about the group:
- The items of a group do not have to follow each other in the markup: other code may stand between them. The group is defined by the
namevalue, not by adjacency. - A page can hold several accordions if each has its own
name. - If several items of a group have
open, according to MDN only the first one in markup order will open. In Chrome 154 we checked a markup where the first item is closed and the second and third haveopen: the second one stayed open. - When we open an item from code, the same rule applies:
details.open = truecloses the other items of the group, and each changed item gets its owntoggleevent.
In a browser without name support the attribute is ignored. The items stay independent, and each still opens and closes. Support for name: Chrome and Edge 120, Firefox 130, Safari 17.2.
How to replace the arrow and style the heading
The browser draws a triangle before summary, the disclosure marker. By default summary behaves like a list item with display: list-item, so the marker is controlled the way a list marker is: list-style: none removes it, and ::marker changes its look. According to MDN, the triangle also disappears when display is changed to block. Safari additionally shows the marker through the non-standard ::-webkit-details-marker, and for it we add display: none to be safe. We draw our own arrow with the ::after pseudo-element.
<section class="faq">
<details class="faq__item" name="faq" open>
<summary class="faq__question">What is an accordion?</summary>
<p class="faq__answer">A list of items: each has a heading, and the text under it is hidden until the item is open.</p>
</details>
<!-- the other items are the same -->
</section>
.faq__question {
display: flex;
justify-content: space-between;
align-items: center;
gap: 16px;
padding: 14px 0;
cursor: pointer;
list-style: none;
}
.faq__question::-webkit-details-marker {
display: none;
}
.faq__question::after {
content: "";
flex: none;
width: 8px;
height: 8px;
border-right: 2px solid currentColor;
border-bottom: 2px solid currentColor;
transform: rotate(45deg);
transition: transform 0.2s;
}
.faq__item[open] > .faq__question::after {
transform: rotate(-135deg);
}
.faq__question:focus-visible {
outline: 2px solid #6aa9ff;
outline-offset: 2px;
}
The arrow turns by the [open] selector: the browser sets this attribute itself, so the item state and the arrow never drift apart. We do not remove the focus ring: without it a person who moves around the page with the Tab key cannot see which item they are on.
The item works from the keyboard without extra code. In Chrome 154 we moved to the heading with Tab, Enter opened the item, Space closed it, and a second Space opened it again.
Inside summary it is better to keep only text and simple markup, without headings. According to MDN, in some browsers summary gets the button role, and a nested h3 loses its heading role: a screen reader will not announce it as a heading. If a heading is needed in the page structure, the role has to be checked in the browsers and screen readers that matter.
What to keep in mind: page search and limits
details has one more property: text inside a closed item can be found with the page search. According to the MDN compatibility tables, the search opens the item with the found text in Chrome and Edge from version 97, in Firefox from version 139 and in Safari from version 26.2. In Firefox before version 148 and in Safari 26.2 there is a bug: the page may not scroll to the found place.
There are limits too:
- On its own
detailsdoes not animate the item: the switch is instant. Smoothness is added by the CSS in the next section. - If an item needs complex behavior, for example loading from a server or closing on a condition, we need a script on the
toggleevent. - To expand or collapse all items with one button we need a script with the
details.openproperty. Items with a sharednamewill not open together this way: one stays open.
How to open and close an item smoothly
The browser keeps the content of details in a separate part, and CSS got the ::details-content pseudo-element for it. While the item is closed, this part has content-visibility: hidden, and on opening it changes to visible. In Chrome 154 we read this value on a closed and an open item through getComputedStyle: hidden and visible. Without a transition the content disappears instantly on closing, so smoothness rests on three techniques:
block-size: 0andoverflow: clipon the closed item andblock-size: autoon the open one set where the height changes from and to;interpolate-size: allow-keywordsallows animating the height between a number and the keywordauto, as in the article “Smooth appearance of a block in CSS: how to animate display: none”;content-visibilityin the transition list with theallow-discretekeyword keeps the content visible while the closing runs.
.faq__answer {
margin: 0;
padding: 0 0 14px;
}
@supports (interpolate-size: allow-keywords) {
:root {
interpolate-size: allow-keywords;
}
.faq__item::details-content {
block-size: 0;
overflow: clip;
transition: block-size 0.25s ease, content-visibility 0.25s allow-discrete;
}
.faq__item[open]::details-content {
block-size: auto;
}
}
@media (prefers-reduced-motion: reduce) {
.faq__question::after,
.faq__item::details-content {
transition: none;
}
}
The .faq__answer rule with margin: 0 is needed here: a paragraph has outer margins by default, and if they stay, the item jumps at the end of the transition. In Chrome 154 the item height in the last frames changed from 116.5 to 133 pixels, and with margin: 0 it grew smoothly up to 115. We set the bottom spacing through padding. The pen adds page styles and borders between items to this code; they do not affect the behavior.
We measured the height of ::details-content in Chrome 154 for an item with a paragraph of 62 pixels. On opening it grew from 0 to 62 pixels with intermediate values of 18, 48 and 57 pixels, and on closing it returned to 0, and the content stayed visible until the end of the transition.
We put the fallback into the @supports condition. Support differs between the parts: ::details-content is understood by Chrome and Edge 131, Firefox 143 and Safari 18.4, while interpolate-size so far only by Chrome and Edge 129. In Firefox and Safari the condition is false, the block of rules is not applied, and the item works as an ordinary details: it opens and closes at once. We simulated this case in Chrome by replacing the condition with an obviously false one: the item opened and closed at once, without intermediate height values. We did not run the example in Firefox and Safari themselves and rely on the compatibility tables. For readers who set "reduce motion", the prefers-reduced-motion block turns the transitions off.
Below is the final accordion: a group through name, a custom arrow, smooth opening and a fallback. To see the animation better, we open the example in the Penlab editor and put 1s instead of 0.25s in both transition values of .faq__item::details-content.
The easiest way to build your own accordion is a new pen: it is free, and a finished example can be sent as a link. A related native element for windows above the page is covered in the article “How to build a modal window with HTML, CSS and JS: the dialog element”.
FAQ
Do we need JavaScript for an accordion?
No. details opens and closes by itself, the name attribute makes an accordion with one open item, and CSS provides the smoothness. A script is needed when code has to react to an item opening: for that there is the toggle event.
How to open an item by default?
We set the open attribute: <details open>. If several items of a group with one name are open, according to MDN the first one in markup order will open.
Can all items of a group be closed?
Yes. A group with name allows no more than one open item, but does not require that one is open. In Chrome 154 a click on the open item closed it, and the group had no open items.
How to open an item from JavaScript?
We write details.open = true. In a group with name the other items close. We checked it in Chrome 154: the item opened, the previous one closed, and each got a toggle event.
What happens in an old browser?
The name attribute without support is ignored: the items work separately. The block with ::details-content in our code sits under @supports, so without interpolate-size support the item opens without animation.
Why is opening smooth but closing instant?
It is worth checking the transition list: it must include content-visibility with allow-discrete. According to MDN, without such a transition the content disappears instantly on closing.