Skip to content

Accordion

AccordionOne section open at a time. Opening another closes it; the open one closes on a click.
Thymeleaf source
<sl:accordion name="accordion-demo">
<sl:accordion-item open>
<sl:accordion-trigger>Is it accessible?</sl:accordion-trigger>
<sl:accordion-content>Yes. Each section is a details element, which the browser opens and closes, from the
keyboard too.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>Is it styled?</sl:accordion-trigger>
<sl:accordion-content>Yes. It comes with default styles that match the other components.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>Is it animated?</sl:accordion-trigger>
<sl:accordion-content>Yes, where the browser can animate a height to auto, and only when the user has not
asked for reduced motion.</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>
Rendered HTML
<div class="accordion">
<details class="accordion-item" name="accordion-demo" open>
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">Is it accessible?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Yes. Each section is a details element, which the browser opens and closes, from the keyboard too.</div>
</details>
<details class="accordion-item" name="accordion-demo">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">Is it styled?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Yes. It comes with default styles that match the other components.</div>
</details>
<details class="accordion-item" name="accordion-demo">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">Is it animated?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Yes, where the browser can animate a height to auto, and only when the user has not asked for reduced motion.</div>
</details>
</div>

An accordion is <sl:accordion> holding <sl:accordion-item>s, each with an <sl:accordion-trigger> and an <sl:accordion-content>, as in shadcn/ui:

<sl:accordion>
<sl:accordion-item open>
<sl:accordion-trigger>Is it accessible?</sl:accordion-trigger>
<sl:accordion-content>Yes. It adheres to the WAI-ARIA design pattern.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>Is it styled?</sl:accordion-trigger>
<sl:accordion-content>Yes.</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>

Each item is a <details> element and its trigger is the <summary>, so the accordion needs no JavaScript: the browser opens and closes the sections, from the keyboard too (Tab, then Enter or Space), and a screen reader announces each trigger as expanded or collapsed. The trigger must be the item’s first child.

type="single" (the default) keeps one section open: opening another closes it. The items share a details name, which the browser uses to close the others. The open section closes on a click too, so a single accordion is always what shadcn/ui calls collapsible. The name must be unique on the page; without name each render generates one (sl-accordion- and eight random characters), so an accordion that htmx swaps in never joins one that is already there. Set name yourself when you want stable markup, but not on an accordion that htmx replaces: see below.

type="multiple" opens and closes every section on its own:

Multipletype="multiple" opens and closes each section on its own.
Thymeleaf source
<sl:accordion type="multiple">
<sl:accordion-item open>
<sl:accordion-trigger>How do I reset my password?</sl:accordion-trigger>
<sl:accordion-content>Choose "Forgot password" on the sign-in page and follow the link we email
you.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item open>
<sl:accordion-trigger>Can I change my username?</sl:accordion-trigger>
<sl:accordion-content>Yes, once every 30 days, from your profile settings.</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>
Rendered HTML
<div class="accordion">
<details class="accordion-item" open>
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">How do I reset my password?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Choose &quot;Forgot password&quot; on the sign-in page and follow the link we email you.</div>
</details>
<details class="accordion-item" open>
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">Can I change my username?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Yes, once every 30 days, from your profile settings.</div>
</details>
</div>

open on an item renders it open, where shadcn/ui takes defaultValue. The server decides, for example from a request parameter or a fragment of the URL it rendered a link to:

<sl:accordion>
<sl:accordion-item th:each="question : ${faq}" th:open="${question.id == openQuestion}" th:id="${question.id}">
<sl:accordion-trigger>[[${question.title}]]</sl:accordion-trigger>
<sl:accordion-content th:utext="${question.answerHtml}">Answer</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>

In a single accordion only the first open item stays open. Write the trigger’s text with [[${...}]], not th:text: th:text replaces the trigger’s content, heading and chevron included.

shadcn/ui puts each trigger in an h3. A <summary> must be the first child of its <details>, so it cannot sit in a heading; the heading goes inside it instead. The trigger renders <summary><span role="heading" aria-level="3">…</span><svg …/></summary>: the summary keeps its own role, a disclosure with its expanded state, and screen reader users can still jump from question to question by heading. level sets the level (3 by default); pick the one that fits the page’s outline. For content that is not a list of headed sections, use a collapsible.

The content of a closed section is still in the page. When the browser’s find-in-page, or a link to an element inside it (/faq#refunds), finds a match there, the browser opens the section. This works in Chrome and Edge 97, Firefox 148 and Safari 26.2 (where Safari and Firefox 139 to 147 do not scroll to the match yet); older browsers do not search closed sections.

The section’s height animates open and closed where the browser can animate a height to auto (interpolate-size, Chrome and Edge 129). The height is that of ::details-content, the box around everything but the summary (Chrome 131, Safari 18.4, Firefox 143). Other browsers open and close the section at once, and so does every browser when the user asks for reduced motion. The chevron turns in both cases (not under reduced motion).

In a card
Thymeleaf source
<sl:card>
<sl:card-header>
<sl:card-title level="2">Subscription &amp; billing</sl:card-title>
<sl:card-description>Common questions about your account, plans and payments</sl:card-description>
</sl:card-header>
<sl:card-content>
<sl:accordion name="accordion-billing">
<sl:accordion-item open>
<sl:accordion-trigger>What subscription plans do you offer?</sl:accordion-trigger>
<sl:accordion-content>
<p>Three: Starter, Professional and Enterprise, each with more storage and support.</p>
<p><a href="/plans">Annual billing is available</a> with a 20% discount.</p>
</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>How does billing work?</sl:accordion-trigger>
<sl:accordion-content>At the start of each billing cycle, by card or bank transfer.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>How do I cancel?</sl:accordion-trigger>
<sl:accordion-content>From your account settings, at any time.</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>
</sl:card-content>
</sl:card>
Rendered HTML
<div class="card">
<div class="card-header">
<div class="card-title" role="heading" aria-level="2">Subscription &amp; billing</div>
<div class="card-description">Common questions about your account, plans and payments</div>
</div>
<div class="card-content">
<div class="accordion">
<details class="accordion-item" name="accordion-billing" open>
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">What subscription plans do you offer?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">
<p>Three: Starter, Professional and Enterprise, each with more storage and support.</p>
<p>
<a href="/plans">Annual billing is available</a>
with a 20% discount.
</p>
</div>
</details>
<details class="accordion-item" name="accordion-billing">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">How does billing work?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">At the start of each billing cycle, by card or bank transfer.</div>
</details>
<details class="accordion-item" name="accordion-billing">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">How do I cancel?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">From your account settings, at any time.</div>
</details>
</div>
</div>
</div>

disabled on an item keeps it closed (or open, with open): its trigger fades, leaves the tab order and ignores the pointer. A closed disabled item renders no content, so find-in-page cannot open it either. shadcn/ui does the same by not rendering closed content at all.

DisabledA disabled section cannot be opened, and renders no content while it is closed.
Thymeleaf source
<sl:accordion name="accordion-disabled">
<sl:accordion-item>
<sl:accordion-trigger>Can I access my account history?</sl:accordion-trigger>
<sl:accordion-content>Yes, in the Account history section of your dashboard.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item disabled>
<sl:accordion-trigger>Premium feature information</sl:accordion-trigger>
<sl:accordion-content>Upgrade your plan to see this.</sl:accordion-content>
</sl:accordion-item>
<sl:accordion-item>
<sl:accordion-trigger>How do I update my email address?</sl:accordion-trigger>
<sl:accordion-content>In your account settings; we send a link to the new address.</sl:accordion-content>
</sl:accordion-item>
</sl:accordion>
Rendered HTML
<div class="accordion">
<details class="accordion-item" name="accordion-disabled">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">Can I access my account history?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">Yes, in the Account history section of your dashboard.</div>
</details>
<details class="accordion-item" name="accordion-disabled" data-disabled="true">
<summary class="accordion-trigger" aria-disabled="true" tabindex="-1">
<span class="accordion-trigger-text" role="heading" aria-level="3">Premium feature information</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
</details>
<details class="accordion-item" name="accordion-disabled">
<summary class="accordion-trigger">
<span class="accordion-trigger-text" role="heading" aria-level="3">How do I update my email address?</span>
<svg xmlns="http://www.w3.org/2000/svg" class="icon accordion-trigger-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
</summary>
<div class="accordion-content">In your account settings; we send a link to the new address.</div>
</details>
</div>

An accordion is plain markup, so a swap that replaces it shows it as the server rendered it, and htmx’s history cache keeps the sections the user opened (the open attribute is in the markup it saves).

Leave name out of a single accordion that a swap replaces. htmx inserts the new markup before it removes the old, and the browser closes an open details that joins a group which already has one open: with the same name on both, the section the server rendered open arrives closed. The generated name is new on every render, so it never meets the old one.

  • Arrow keys between the triggers. Radix moves the focus with the up and down arrows, Home and End; the ARIA pattern makes these optional. A details accordion has no script, so Tab moves between the triggers.
  • A single accordion that cannot be closed (shadcn/ui’s single without collapsible): the browser always lets the open details close.
  • value, onValueChange and controlled state: the server renders the open items with open; the browser’s toggle event reports changes.
  • dir and orientation: the accordion follows the page’s direction, and is vertical.
  • role="region" on the content. Radix gives every panel one; the ARIA pattern advises against it where more than about six panels can be open at once, as landmarks pile up. The content is inside the details, which already groups it with its trigger.
AttributeValuesDefaultDescription
typesingle multiplesinglesingle: opening a section closes the one that was open (the items share a details name). multiple: sections open and close on their own.
namestring—The group name the items of a single accordion share. Must be unique on the page. Without it, each render generates one (sl-accordion- and eight random characters).

Every prop can also be set from an expression: th:type="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
openboolean—Render the section open. In a single accordion only the first open section stays open.
disabledboolean—The section cannot be opened or closed: its trigger is out of the tab order and ignores the pointer, and a closed disabled section renders no content, so find-in-page cannot open it.

Every prop can also be set from an expression: th:open="${...}". Any other attribute passes through to the rendered element, and class is appended.

AttributeValuesDefaultDescription
level1 2 3 4 5 63Heading level of the text (role="heading" and aria-level), as shadcn/ui's trigger sits in an h3. Pick the level that fits the page's outline.

Every prop can also be set from an expression: th:level="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:accordion-content declares no props; every attribute passes through.