shshadcn-htmx

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.json

2. Use it

components/ui/exclusive-accordion.tsx
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
components/ui/exclusive-accordion.tsx
/** @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

templates/components/exclusive-accordion.html
{% 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
templates/components/exclusive-accordion.html
{# 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

components/exclusive-accordion.tmpl
{{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
components/exclusive-accordion.tmpl
{{/*
  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

lib/my_app_web/components/exclusive_accordion.ex
<.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
lib/my_app_web/components/exclusive_accordion.ex
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

snippets/exclusive-accordion.html
<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
snippets/exclusive-accordion.html
<!--
  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">&lt;details name&gt;</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?
A small library that turns any HTML attribute into an AJAX trigger — no JSON, no client framework needed.
Why pair it with Tailwind v4?
Utility-first CSS keeps the markup self-explanatory and the bundle small.
Does it work without JavaScript?
Yes — the exclusivity is native <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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[data-slot=exclusive-accordion-chevron]]:rotate-180">
      What&#39;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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[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">&lt;details name&gt;</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?
Orders placed before 2pm ship same day.
What's the return window?
30 days, no questions asked.
Do you ship internationally?
Yes, to 40+ countries. Duties calculated at checkout.
<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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[data-slot=exclusive-accordion-chevron]]:rotate-180">
      What&#39;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 [&amp;::-webkit-details-marker]:hidden [details[open]&gt;&amp;_[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>

PropTypeDefaultDescription
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>
valuestring
Distinct identifier per item, emitted as the data-value attribute so each item is individually targetable. Set on ExclusiveAccordionItem.
openbooleanfalse
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>
disabledbooleanfalse
Visually mute an item and block pointer interaction (pointer-events-none, reduced opacity). Set on ExclusiveAccordionItem.
classstring
Extra Tailwind classes appended to the root element.
hx-*any
Any htmx attribute. Forwarded onto the underlying element.htmxAttribute reference

* required