Components
Exclusive Accordion
The scriptless single-open accordion. Several <details> share one name attribute, so opening one auto-closes the others — the pure-HTML exclusive variant of the APG-scripted accordion. Zero JavaScript: the exclusivity survives with JS disabled.
Installation
1. Install via the shadcn CLI
npx shadcn@latest add http://localhost/r/exclusive-accordion.json2. Use it
import { ExclusiveAccordion, ExclusiveAccordionItem,
ExclusiveAccordionTrigger, ExclusiveAccordionContent
} from "@/components/ui/exclusive-accordion"
<ExclusiveAccordion name="faq">
<ExclusiveAccordionItem name="faq" value="q1" open>
<ExclusiveAccordionTrigger>What's htmx?</ExclusiveAccordionTrigger>
<ExclusiveAccordionContent>Hypermedia-driven HTML.</ExclusiveAccordionContent>
</ExclusiveAccordionItem>
<ExclusiveAccordionItem name="faq" value="q2">
<ExclusiveAccordionTrigger>Why Tailwind v4?</ExclusiveAccordionTrigger>
<ExclusiveAccordionContent>Utility-first, small bundles.</ExclusiveAccordionContent>
</ExclusiveAccordionItem>
</ExclusiveAccordion>Or copy the source manually
/** @jsxImportSource hono/jsx */
import type { PropsWithChildren } from "hono/jsx"
import { cn, type ClassValue } from "@/registry/lib/cn"
// Exclusive Accordion — shadcn-htmx, htmx v4 + Tailwind v4.
//
// The scriptless, single-open accordion. Several <details> elements share one
// `name` attribute, so the browser keeps exactly one open at a time: opening
// any item auto-closes the others. This is the pure-HTML exclusive variant of
// the APG-scripted accordion — ZERO JavaScript, no boot script, no site.js.
//
// shadcn upstream uses Radix Accordion (a JS state machine wiring buttons +
// regions with aria-expanded / aria-controls). We let the platform do it:
// - Click / Space / Enter toggles open (browser default on <summary>).
// - <summary> is implicitly role="button", focusable, with its text as the
// accessible name; the browser sets aria-expanded to mirror `open`.
// - <details name="..."> makes the group mutually exclusive natively. Per the
// HTML spec, if more than one grouped item carries `open`, only the FIRST
// in source order renders open — so we never produce an invalid state.
//
// This component differs from registry/ui/accordion.tsx (type="single"), which
// assigns the grouping `name` at runtime via public/site.js and layers the APG
// arrow-key contract on top. ExclusiveAccordion renders the `name` straight
// into the server HTML, so the exclusivity survives with JS disabled and there
// is no keyboard contract beyond what <summary> ships natively (Tab to focus,
// Enter / Space to toggle). That makes it the right pick for progressive-
// enhancement-first surfaces (docs FAQs, server-rendered settings panels).
//
// Refs:
// repos/mdn/files/en-us/web/html/reference/elements/details/index.md
// (`name` attribute — "give multiple <details> the same name value to
// group them. Only one of the grouped <details> can be open at a time …
// if multiple are given `open`, only the first in source order renders
// open." Also: `open` boolean, the `toggle` event, implicit role=group.)
// repos/mdn/files/en-us/web/html/reference/elements/summary/index.md
// (click / Space toggles parent <details>; display:list-item marker.)
// repos/aria-practices/content/patterns/accordion/ (the scripted contract we
// deliberately do NOT emulate here — see the note above.)
// repos/aria-practices/content/patterns/disclosure/ (native <details> is a
// disclosure widget; aria-controls is optional.)
type ExclusiveAccordionProps = PropsWithChildren<{
// Shared group name written onto every item's <details name>. Required —
// it is what makes the group exclusive. Distinct accordions on one page
// must use distinct names or they'd close each other.
name: string
class?: ClassValue
}>
export function ExclusiveAccordion(props: ExclusiveAccordionProps) {
const { name, class: className, children, ...rest } = props
return (
<div
data-slot="exclusive-accordion"
data-name={name}
class={cn("w-full", className)}
{...rest}
>
{children}
</div>
)
}
type ExclusiveAccordionItemProps = PropsWithChildren<{
// The shared group name. Pass the SAME value as the parent's `name`.
name: string
// Distinct identifier per item, emitted as data-value for targeting.
value?: string
// Pre-open this item on initial render. If two items in the group set this,
// the browser opens only the first in source order (HTML spec).
open?: boolean
disabled?: boolean
class?: ClassValue
}>
export function ExclusiveAccordionItem(props: ExclusiveAccordionItemProps) {
const { name, value, open, disabled, class: className, children, ...rest } =
props
return (
<details
data-slot="exclusive-accordion-item"
data-value={value}
data-disabled={disabled ? "true" : undefined}
name={name}
open={open}
class={cn(
"border-b last:border-b-0",
disabled && "pointer-events-none opacity-50",
className,
)}
{...rest}
>
{children}
</details>
)
}
type ExclusiveAccordionTriggerProps = PropsWithChildren<{ class?: ClassValue }>
export function ExclusiveAccordionTrigger(
props: ExclusiveAccordionTriggerProps,
) {
const { class: className, children, ...rest } = props
return (
<summary
data-slot="exclusive-accordion-trigger"
class={cn(
"flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden " +
"hover:underline " +
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 " +
"list-none [&::-webkit-details-marker]:hidden " +
// Rotate the chevron when the parent <details> is open.
"[details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180",
className,
)}
{...rest}
>
{children}
<svg
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
data-slot="exclusive-accordion-chevron"
class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200"
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
)
}
type ExclusiveAccordionContentProps = PropsWithChildren<{ class?: ClassValue }>
export function ExclusiveAccordionContent(
props: ExclusiveAccordionContentProps,
) {
const { class: className, children, ...rest } = props
return (
<div
data-slot="exclusive-accordion-content"
class={cn("overflow-hidden pt-0 pb-4 text-sm", className)}
{...rest}
>
{children}
</div>
)
}
1. Save the file
Copy exclusive-accordion.html into templates/components/.
2. Use it
{% from "components/exclusive-accordion.html" import
exclusive_accordion_open, exclusive_accordion_close,
exclusive_accordion_item_open, exclusive_accordion_item_close,
exclusive_accordion_trigger,
exclusive_accordion_content_open, exclusive_accordion_content_close %}
{{ exclusive_accordion_open(name="faq") }}
{{ exclusive_accordion_item_open(name="faq", value="q1", open=true) }}
{{ exclusive_accordion_trigger("What's htmx?") }}
{{ exclusive_accordion_content_open() }}
Hypermedia-driven HTML.
{{ exclusive_accordion_content_close() }}
{{ exclusive_accordion_item_close() }}
{{ exclusive_accordion_close() }}View source
{# Exclusive Accordion macros — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/exclusive-accordion.tsx EXACTLY.
The scriptless single-open accordion: every <details> shares one `name`
attribute, so the browser keeps exactly one open at a time — opening one
auto-closes the others. ZERO JavaScript: no boot script, no site.js. Per
the HTML spec, if multiple grouped items carry `open`, only the first in
source order renders open.
Ref: repos/mdn/files/en-us/web/html/reference/elements/details/index.md (name)
Usage:
{% from "components/exclusive-accordion.html" import
exclusive_accordion_open, exclusive_accordion_close,
exclusive_accordion_item_open, exclusive_accordion_item_close,
exclusive_accordion_trigger,
exclusive_accordion_content_open, exclusive_accordion_content_close %}
{{ exclusive_accordion_open(name="faq") }}
{{ exclusive_accordion_item_open(name="faq", value="q1", open=true) }}
{{ exclusive_accordion_trigger("What's htmx?") }}
{{ exclusive_accordion_content_open() }}
Hypermedia-driven HTML extensions.
{{ exclusive_accordion_content_close() }}
{{ exclusive_accordion_item_close() }}
{{ exclusive_accordion_close() }} #}
{% macro exclusive_accordion_open(name, extra_class="") -%}
<div data-slot="exclusive-accordion" data-name="{{ name }}" class="w-full {{ extra_class }}">
{%- endmacro %}
{% macro exclusive_accordion_close() %}</div>{% endmacro %}
{% macro exclusive_accordion_item_open(name, value="", open=false, disabled=false, extra_class="") -%}
<details data-slot="exclusive-accordion-item"
{%- if value %} data-value="{{ value }}"{% endif %}
{%- if disabled %} data-disabled="true"{% endif %}
name="{{ name }}"
{%- if open %} open{% endif %}
class="border-b last:border-b-0 {% if disabled %}pointer-events-none opacity-50{% endif %} {{ extra_class }}">
{%- endmacro %}
{% macro exclusive_accordion_item_close() %}</details>{% endmacro %}
{% macro exclusive_accordion_trigger(text, extra_class="") -%}
<summary data-slot="exclusive-accordion-trigger"
class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180 {{ extra_class }}">
{{ text }}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron"
class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
{%- endmacro %}
{% macro exclusive_accordion_content_open(extra_class="") -%}
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm {{ extra_class }}">
{%- endmacro %}
{% macro exclusive_accordion_content_close() %}</div>{% endmacro %}
1. Save the file
Add exclusive-accordion.tmpl alongside your templates.
2. Use it
{{template "exclusive_accordion" (dict
"Name" "faq"
"Body" (htmlSafe `
{{template "exclusive_accordion_item" (dict
"Name" "faq" "Value" "q1" "Title" "What's htmx?" "Open" true
"Body" (htmlSafe "Hypermedia-driven HTML.")
)}}`)
)}}View source
{{/*
Exclusive Accordion templates — shadcn-htmx, htmx v4 + Tailwind v4.
Mirrors registry/ui/exclusive-accordion.tsx EXACTLY.
The scriptless single-open accordion: every <details> shares one `name`
attribute, so the browser keeps exactly one open at a time — opening one
auto-closes the others. ZERO JavaScript. Per the HTML spec, if multiple
grouped items carry `open`, only the first in source order renders open.
Ref: repos/mdn/files/en-us/web/html/reference/elements/details (name)
Three named templates compose:
- "exclusive_accordion" — wrapper open/close (call with Name + Body)
- "exclusive_accordion_item" — single <details name> with summary + content
- "exclusive_accordion_trigger" — just the <summary> (when composing manually)
Usage:
{{template "exclusive_accordion" (dict
"Name" "faq"
"Body" (htmlSafe `
{{template "exclusive_accordion_item" (dict
"Name" "faq" "Value" "q1" "Title" "What's htmx?" "Open" true
"Body" (htmlSafe "Hypermedia-driven HTML extensions.")
)}}`)
)}}
*/}}
{{define "exclusive_accordion"}}
<div data-slot="exclusive-accordion" data-name="{{.Name}}" class="w-full">
{{.Body}}
</div>
{{end}}
{{define "exclusive_accordion_item"}}
<details data-slot="exclusive-accordion-item"
{{if .Value}}data-value="{{.Value}}"{{end}}
{{if .Disabled}}data-disabled="true"{{end}}
name="{{.Name}}"
{{if .Open}}open{{end}}
class="border-b last:border-b-0">
{{template "exclusive_accordion_trigger" (dict "Text" .Title)}}
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">{{.Body}}</div>
</details>
{{end}}
{{define "exclusive_accordion_trigger"}}
<summary data-slot="exclusive-accordion-trigger"
class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
{{.Text}}
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron"
class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
{{end}}
1. Save the file
Drop exclusive_accordion.ex into lib/my_app_web/components/.
2. Use it
<.exclusive_accordion name="faq">
<.exclusive_accordion_item name="faq" value="q1" open>
<.exclusive_accordion_trigger>What's htmx?</.exclusive_accordion_trigger>
<.exclusive_accordion_content>Hypermedia-driven HTML.</.exclusive_accordion_content>
</.exclusive_accordion_item>
</.exclusive_accordion>View source
defmodule ShadcnHtmx.Components.ExclusiveAccordion do
@moduledoc """
Exclusive Accordion — shadcn-htmx, htmx v4 + Tailwind v4 for Phoenix.
Mirrors registry/ui/exclusive-accordion.tsx EXACTLY.
The scriptless single-open accordion. Several `<details>` elements share one
`name` attribute, so the browser keeps exactly one open at a time — opening
one auto-closes the others. ZERO JavaScript: no boot script, no site.js. Per
the HTML spec, if multiple grouped items carry `open`, only the first in
source order renders open.
Ref: repos/mdn/files/en-us/web/html/reference/elements/details (name)
## Examples
<.exclusive_accordion name="faq">
<.exclusive_accordion_item name="faq" value="q1" open>
<.exclusive_accordion_trigger>What's htmx?</.exclusive_accordion_trigger>
<.exclusive_accordion_content>Hypermedia-driven HTML extensions.</.exclusive_accordion_content>
</.exclusive_accordion_item>
</.exclusive_accordion>
"""
use Phoenix.Component
attr :name, :string, required: true
attr :class, :string, default: nil
slot :inner_block, required: true
def exclusive_accordion(assigns) do
~H"""
<div
data-slot="exclusive-accordion"
data-name={@name}
class={["w-full", @class]}
>
{render_slot(@inner_block)}
</div>
"""
end
attr :name, :string, required: true
attr :value, :string, default: nil
attr :open, :boolean, default: false
attr :disabled, :boolean, default: false
attr :class, :string, default: nil
slot :inner_block, required: true
def exclusive_accordion_item(assigns) do
~H"""
<details
data-slot="exclusive-accordion-item"
data-value={@value}
data-disabled={@disabled && "true"}
name={@name}
open={@open}
class={[
"border-b last:border-b-0",
@disabled && "pointer-events-none opacity-50",
@class
]}
>
{render_slot(@inner_block)}
</details>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
def exclusive_accordion_trigger(assigns) do
~H"""
<summary
data-slot="exclusive-accordion-trigger"
class={[
"flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium",
"transition-all outline-none select-none marker:hidden hover:underline",
"focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
"list-none [&::-webkit-details-marker]:hidden",
"[details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180",
@class
]}
>
{render_slot(@inner_block)}
<svg
xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron"
class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200"
aria-hidden="true"
>
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
"""
end
attr :class, :string, default: nil
slot :inner_block, required: true
def exclusive_accordion_content(assigns) do
~H"""
<div data-slot="exclusive-accordion-content" class={["overflow-hidden pt-0 pb-4 text-sm", @class]}>
{render_slot(@inner_block)}
</div>
"""
end
end
1. Save the file
Paste the markup; relies only on theme tokens. No script.
2. Use it
<div data-slot="exclusive-accordion" data-name="faq" class="w-full">
<details name="faq" data-slot="exclusive-accordion-item" data-value="q1" open class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="…">What's htmx? <svg …chevron/></summary>
<div data-slot="exclusive-accordion-content" class="…">Hypermedia-driven HTML.</div>
</details>
<details name="faq" data-slot="exclusive-accordion-item" data-value="q2" class="border-b last:border-b-0">…</details>
</div>View source
<!--
shadcn-htmx — raw HTML exclusive-accordion snippet.
Mirrors registry/ui/exclusive-accordion.tsx EXACTLY.
The scriptless single-open accordion: every <details> shares one `name`
attribute, so the browser keeps exactly one open at a time — opening one
auto-closes the others. NO JavaScript, no boot script. Relies only on theme
tokens. Per the HTML spec, if multiple items carry `open`, only the first in
source order renders open.
Ref: https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/details#name
Distinct accordions on one page must use distinct `name` values, or opening
an item in one closes an item in the other.
-->
<div data-slot="exclusive-accordion" data-name="faq" class="w-full">
<details name="faq" data-slot="exclusive-accordion-item" data-value="q1" open class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger"
class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
What's htmx?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">
A small library that turns any HTML attribute into an AJAX trigger — no JSON, no client framework needed.
</div>
</details>
<details name="faq" data-slot="exclusive-accordion-item" data-value="q2" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger"
class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
Why pair it with Tailwind v4?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">
Utility-first CSS keeps the markup self-explanatory and the bundle small.
</div>
</details>
<details name="faq" data-slot="exclusive-accordion-item" data-value="q3" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger"
class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
Does it work without JavaScript?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"
data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9" />
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">
Yes — the exclusivity is the native <code class="rounded bg-muted px-1 py-0.5"><details name></code> grouping. Zero script required.
</div>
</details>
</div>
Examples
Single-open — only one item expands at a time
Every item shares the same name. Opening one auto-closes the rest. No JavaScript runs.
The HTML Living Standard added the name attribute on <details> for exactly this pattern: give several items the same name and they become mutually exclusive — opening one closes the others, with no state machine and no client framework. Unlike the scripted Accordion this carries no APG arrow-key contract; the only keyboard interaction is what <summary> ships natively (Tab to focus, Enter / Space to toggle).
What's htmx?
Why pair it with Tailwind v4?
Does it work without JavaScript?
<details name> grouping. Disable JS and it still keeps one open.<ExclusiveAccordion name="faq">
<ExclusiveAccordionItem name="faq" value="q1" open>…</ExclusiveAccordionItem>
<ExclusiveAccordionItem name="faq" value="q2">…</ExclusiveAccordionItem>
</ExclusiveAccordion>{{ exclusive_accordion_open(name="faq") }}
{{ exclusive_accordion_item_open(name="faq", value="q1", open=true) }}…{{ exclusive_accordion_item_close() }}
{{ exclusive_accordion_item_open(name="faq", value="q2") }}…{{ exclusive_accordion_item_close() }}
{{ exclusive_accordion_close() }}{{template "exclusive_accordion" (dict "Name" "faq" "Body" (htmlSafe `…`))}}<.exclusive_accordion name="faq">
<.exclusive_accordion_item name="faq" value="q1" open>…</.exclusive_accordion_item>
<.exclusive_accordion_item name="faq" value="q2">…</.exclusive_accordion_item>
</.exclusive_accordion><div data-slot="exclusive-accordion" data-name="ex-xacc-basic" class="w-full max-w-md">
<details data-slot="exclusive-accordion-item" data-value="q1" name="ex-xacc-basic" open="" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
What's htmx?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">
A small library that turns any HTML attribute into an AJAX trigger — no JSON, no client framework needed.
</div>
</details>
<details data-slot="exclusive-accordion-item" data-value="q2" name="ex-xacc-basic" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
Why pair it with Tailwind v4?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">Utility-first CSS keeps the markup self-explanatory and the bundle small.</div>
</details>
<details data-slot="exclusive-accordion-item" data-value="q3" name="ex-xacc-basic" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
Does it work without JavaScript?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">
Yes — the exclusivity is native
<code class="rounded bg-muted px-1 py-0.5"><details name></code>
grouping. Disable JS and it still keeps one open.
</div>
</details>
</div>FAQ — start fully collapsed
Omit open on every item to start with nothing expanded. The first click opens one; the next swaps the open item.
A frequently-asked-questions list is the canonical use: the reader opens one answer at a time and the previous answer tucks away on its own. Because exclusivity is enforced by the browser, you never have to reconcile open state on the server after an htmx swap — newly inserted items that share the name join the group automatically.
How fast do you ship?
What's the return window?
Do you ship internationally?
<ExclusiveAccordion name="faq">
<ExclusiveAccordionItem name="faq" value="ship">…</ExclusiveAccordionItem>
<ExclusiveAccordionItem name="faq" value="return">…</ExclusiveAccordionItem>
</ExclusiveAccordion>{{ exclusive_accordion_open(name="faq") }}
{{ exclusive_accordion_item_open(name="faq", value="ship") }}…{{ exclusive_accordion_item_close() }}
{{ exclusive_accordion_close() }}{{template "exclusive_accordion" (dict "Name" "faq" "Body" (htmlSafe `…`))}}<.exclusive_accordion name="faq">
<.exclusive_accordion_item name="faq" value="ship">…</.exclusive_accordion_item>
</.exclusive_accordion><div data-slot="exclusive-accordion" data-name="ex-xacc-faq" class="w-full max-w-md">
<details data-slot="exclusive-accordion-item" data-value="ship" name="ex-xacc-faq" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
How fast do you ship?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">Orders placed before 2pm ship same day.</div>
</details>
<details data-slot="exclusive-accordion-item" data-value="return" name="ex-xacc-faq" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
What's the return window?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">30 days, no questions asked.</div>
</details>
<details data-slot="exclusive-accordion-item" data-value="intl" name="ex-xacc-faq" class="border-b last:border-b-0">
<summary data-slot="exclusive-accordion-trigger" class="flex cursor-pointer items-start justify-between gap-4 rounded-md py-4 text-left text-sm font-medium transition-all outline-none select-none marker:hidden hover:underline focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 list-none [&::-webkit-details-marker]:hidden [details[open]>&_[data-slot=exclusive-accordion-chevron]]:rotate-180">
Do you ship internationally?
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" data-slot="exclusive-accordion-chevron" class="pointer-events-none size-4 shrink-0 translate-y-0.5 text-muted-foreground transition-transform duration-200" aria-hidden="true">
<polyline points="6 9 12 15 18 9">
</polyline>
</svg>
</summary>
<div data-slot="exclusive-accordion-content" class="overflow-hidden pt-0 pb-4 text-sm">Yes, to 40+ countries. Duties calculated at checkout.</div>
</details>
</div>Further reading
API Reference
<ExclusiveAccordion>
| Prop | Type | Default | Description |
|---|---|---|---|
name* | string | — | Shared group name written onto every item's <details name>. Required — it is what makes the group exclusive. Pass the same value to the root and to each item. Distinct accordions on one page must use distinct names, or they'd close each other.MDN<details name> |
value | string | — | Distinct identifier per item, emitted as the data-value attribute so each item is individually targetable. Set on ExclusiveAccordionItem. |
open | boolean | false | Render this item expanded on initial load. Maps to the native boolean <details open> attribute — omit it (don't pass the string "false") to start collapsed. If two items in the group set open, the browser opens only the first in source order.MDN<details open> |
disabled | boolean | false | Visually mute an item and block pointer interaction (pointer-events-none, reduced opacity). Set on ExclusiveAccordionItem. |
class | string | — | Extra Tailwind classes appended to the root element. |
hx-* | any | — | Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference |
* required