Skip to content
data-slotv1.0.0
Esc
↑↓navigate↵open⌘Jpreview
On this page

Accordion

A set of collapsible sections that reveal one panel or several at a time.

@data-slot/accordionSource ↗
bun add @data-slot/accordion
npm install @data-slot/accordion
pnpm add @data-slot/accordion

Anatomy

One accordion-item per section, each with its own data-value — items without one are skipped. Inside an item, the trigger and content are wired together for you. accordion-trigger-icon and accordion-content-inner are optional styling hooks.

<div data-slot="accordion">
  <div data-slot="accordion-item" data-value="unique-id">
    <button data-slot="accordion-trigger">
      <span>Trigger</span>
      <span data-slot="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content">
      <div data-slot="accordion-content-inner">Content</div>
    </div>
  </div>
</div>

Examples

Single panel

Preview
Unlike React-based headless libraries, data-slot works with vanilla HTML. Just add data-slot attributes to your markup and call create(). No framework, no virtual DOM, no build step required.
Yes. All components implement WAI-ARIA patterns with proper roles, keyboard navigation, and focus management out of the box.
No dependencies. Each component is standalone and tree-shakeable.
Unlike React-based headless libraries, data-slot works with vanilla HTML. Just add data-slot attributes to your markup and call create(). No framework, no virtual DOM, no build step required.
Yes. All components implement WAI-ARIA patterns with proper roles, keyboard navigation, and focus management out of the box.
No dependencies. Each component is standalone and tree-shakeable.
Show codeHide code
<div data-slot="accordion" data-default-value="features">
  <div data-slot="accordion-item" data-value="features" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">
      <span>What makes data-slot different?</span>
      <span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content" class="accordion-content">
      <div data-slot="accordion-content-inner" class="accordion-content-inner">
        Unlike React-based headless libraries, data-slot works with vanilla HTML.
      </div>
    </div>
  </div>
  <div data-slot="accordion-item" data-value="accessibility" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">
      <span>Is it accessible?</span>
      <span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content" class="accordion-content">
      <div data-slot="accordion-content-inner" class="accordion-content-inner">
        Yes. All components implement WAI-ARIA patterns.
      </div>
    </div>
  </div>
</div>

<style>
  .accordion-item { border-bottom: 1px dashed var(--border); }
  .accordion-item:last-child { border-bottom: none; }
  .accordion-trigger {
    width: 100%;
    padding: 0.75rem 0;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 1rem;
  }
  .accordion-trigger-icon {
    margin-left: auto;
    color: var(--muted);
    transition: transform 0.25s ease;
  }
  .accordion-item[data-state="open"] .accordion-trigger-icon {
    transform: rotate(45deg);
  }
  .accordion-content {
    overflow: hidden;
    height: var(--accordion-panel-height);
    transition: height 0.3s ease-out;
  }
  .accordion-content[data-starting-style] {
    height: 0;
  }
  .accordion-content-inner { padding: 0 0 1rem; color: var(--muted); }
</style>
<div data-slot="accordion" data-default-value="features">
  <div
    data-slot="accordion-item"
    data-value="features"
    class="group border-b border-dashed border-[var(--border)] last:border-b-0"
  >
    <button
      data-slot="accordion-trigger"
      class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
    >
      <span>What makes data-slot different?</span>
      <span
        data-slot="accordion-trigger-icon"
        class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
      >
        +
      </span>
    </button>
    <div
      data-slot="accordion-content"
      class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
    >
      <div
        data-slot="accordion-content-inner"
        class="pb-4 pt-0 text-[var(--muted)]"
      >
        Unlike React-based headless libraries, data-slot works with vanilla HTML.
      </div>
    </div>
  </div>
  <div
    data-slot="accordion-item"
    data-value="accessibility"
    class="group border-b border-dashed border-[var(--border)] last:border-b-0"
  >
    <button
      data-slot="accordion-trigger"
      class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
    >
      <span>Is it accessible?</span>
      <span
        data-slot="accordion-trigger-icon"
        class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
      >
        +
      </span>
    </button>
    <div
      data-slot="accordion-content"
      class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
    >
      <div
        data-slot="accordion-content-inner"
        class="pb-4 pt-0 text-[var(--muted)]"
      >
        Yes. All components implement WAI-ARIA patterns.
      </div>
    </div>
  </div>
</div>

Multiple panels

Add data-multiple to let several sections stay open at once.

Preview
With multiple: true, multiple items can be open simultaneously.
Try clicking both items — they stay open independently.
With multiple: true, multiple items can be open simultaneously.
Try clicking both items — they stay open independently.
Show codeHide code
<div data-slot="accordion" data-multiple>
  <div data-slot="accordion-item" data-value="item1" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">
      <span>First item</span>
      <span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content" class="accordion-content">
      <div data-slot="accordion-content-inner" class="accordion-content-inner">
        With <code>multiple: true</code>, multiple items can be open simultaneously.
      </div>
    </div>
  </div>
  <div data-slot="accordion-item" data-value="item2" class="accordion-item">
    <button data-slot="accordion-trigger" class="accordion-trigger">
      <span>Second item</span>
      <span data-slot="accordion-trigger-icon" class="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content" class="accordion-content">
      <div data-slot="accordion-content-inner" class="accordion-content-inner">
        Try clicking both items — they stay open independently.
      </div>
    </div>
  </div>
</div>

<style>
  .accordion-item { border-bottom: 1px dashed var(--border); }
  .accordion-item:last-child { border-bottom: none; }
  .accordion-trigger {
    width: 100%;
    padding: 0.75rem 0;
    background: none;
    border: none;
    text-align: left;
    cursor: pointer;
    display: flex;
    align-items: center;
    gap: 1rem;
  }
  .accordion-trigger-icon {
    margin-left: auto;
    color: var(--muted);
    transition: transform 0.25s ease;
  }
  .accordion-item[data-state="open"] .accordion-trigger-icon {
    transform: rotate(45deg);
  }
  .accordion-content {
    overflow: hidden;
    height: var(--accordion-panel-height);
    transition: height 0.3s ease-out;
  }
  .accordion-content[data-starting-style] {
    height: 0;
  }
  .accordion-content-inner { padding: 0 0 1rem; color: var(--muted); }
</style>
<div data-slot="accordion" data-multiple>
  <div
    data-slot="accordion-item"
    data-value="item1"
    class="group border-b border-dashed border-[var(--border)] last:border-b-0"
  >
    <button
      data-slot="accordion-trigger"
      class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
    >
      <span>First item</span>
      <span
        data-slot="accordion-trigger-icon"
        class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
      >
        +
      </span>
    </button>
    <div
      data-slot="accordion-content"
      class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
    >
      <div
        data-slot="accordion-content-inner"
        class="pb-4 pt-0 text-[var(--muted)]"
      >
        With <code>multiple: true</code>, multiple items can be open simultaneously.
      </div>
    </div>
  </div>
  <div
    data-slot="accordion-item"
    data-value="item2"
    class="group border-b border-dashed border-[var(--border)] last:border-b-0"
  >
    <button
      data-slot="accordion-trigger"
      class="flex w-full items-center gap-4 bg-transparent py-3 text-left"
    >
      <span>Second item</span>
      <span
        data-slot="accordion-trigger-icon"
        class="ml-auto shrink-0 text-[var(--muted)] transition-transform group-data-[state=open]:rotate-45"
      >
        +
      </span>
    </button>
    <div
      data-slot="accordion-content"
      class="overflow-hidden h-(--accordion-panel-height) transition-all duration-300 ease-out data-starting-style:h-0"
    >
      <div
        data-slot="accordion-content-inner"
        class="pb-4 pt-0 text-[var(--muted)]"
      >
        Try clicking both items — they stay open independently.
      </div>
    </div>
  </div>
</div>

API reference

Initialization

create(scope?)

Auto-discover and bind all accordion instances in a scope (defaults to document).

import { create } from "@data-slot/accordion";

const controllers = create(); // Returns AccordionController[]

createAccordion(root, options?)

Create a controller for a specific element.

import { createAccordion } from "@data-slot/accordion";

const accordion = createAccordion(element, {
  multiple: true,
  defaultValue: ["one"],
  orientation: "vertical",
  loopFocus: true,
  hiddenUntilFound: false,
  onValueChange: (values) => console.log(values),
});

Slots

Runtime Slots

  • accordion - Root element that manages expanded items and keyboard navigation.
  • accordion-item - Container for one section. Set a unique data-value to identify it; at least one item is required.
  • accordion-trigger - Button inside an item that toggles its content and receives aria-expanded.
  • accordion-content - Panel inside an item, linked to its trigger and shown when the item is expanded.

Style-only Slots

  • accordion-trigger-icon - Optional icon inside the trigger, useful for rotating with the open state.
  • accordion-content-inner - Optional wrapper for content padding inside the animated panel.

Markup

<div data-slot="accordion">
  <div data-slot="accordion-item" data-value="unique-id">
    <button data-slot="accordion-trigger">
      <span>Trigger</span>
      <span data-slot="accordion-trigger-icon">+</span>
    </button>
    <div data-slot="accordion-content">
      <div data-slot="accordion-content-inner">Content</div>
    </div>
  </div>
</div>

Data Attributes

Options can also be set via data attributes on the root element. JS options take precedence.

Attribute Type Default Description
data-multiple boolean false Allow multiple items open at once
data-default-value string none Initially expanded item, or a JSON array string for multiple defaults
data-disabled boolean false Disable the entire accordion
data-orientation "horizontal" | "vertical" "vertical" Controls roving-focus arrow keys
data-loop-focus boolean true Wrap roving focus at the ends
data-hidden-until-found boolean false Use hidden="until-found" on closed panels
data-collapsible boolean true Deprecated single-mode compatibility alias

Boolean attributes: present or "true" = true, "false" = false, absent = default.

<div
  data-slot="accordion"
  data-multiple
  data-default-value="one"
  data-orientation="horizontal"
>
  ...
</div>

For multiple default items in HTML, encode the value as JSON:

<div
  data-slot="accordion"
  data-multiple
  data-default-value='["one","two"]'
>
  ...
</div>

Options

Option Type Default Description
multiple boolean false Allow multiple items open at once
defaultValue string | string[] undefined Initially expanded item(s)
disabled boolean false Disable all user interaction for the accordion
orientation "horizontal" | "vertical" "vertical" Controls roving-focus arrow keys
loopFocus boolean true Wrap roving focus at the ends
hiddenUntilFound boolean false Use hidden="until-found" on closed panels
onValueChange (value: string[]) => void undefined Callback when expanded items change
collapsible boolean true Deprecated single-mode alias for “can close the last open item”

Deprecated Option

The following option is deprecated and will be removed in the next major release:

createAccordion(element, {
  // Deprecated: use the default Base UI-style collapsible behavior instead.
  collapsible: false,
});

Controller

Method/Property Description
expand(value) Expand an item by value
collapse(value) Collapse an item by value
toggle(value) Toggle an item by value
value Currently expanded values (readonly string[])
destroy() Cleanup all event listeners

Events

Outbound Events

Listen for changes via custom events:

element.addEventListener("accordion:change", (e) => {
  console.log("Expanded items:", e.detail.value);
});

Inbound Events

Control the accordion via events:

Event Detail Description
accordion:set { value: string | string[] } Set expanded items programmatically
element.dispatchEvent(
  new CustomEvent("accordion:set", { detail: { value: "one" } })
);

element.dispatchEvent(
  new CustomEvent("accordion:set", { detail: { value: ["one", "two"] } })
);

accordion:set and controller methods still work when the accordion is disabled. User-triggered click and keyboard interaction do not.

Styling

State Hooks

The accordion exposes these useful styling hooks:

Element Hooks
root data-disabled, data-orientation
item data-state, data-open, data-closed, data-index, data-disabled
trigger data-state, data-panel-open, data-disabled, aria-expanded
content data-state, data-open, data-closed, data-index, data-disabled, data-orientation, data-starting-style, data-ending-style

CSS Variables

The content element exposes size variables for height or width transitions:

Variable Description
--accordion-panel-height Panel height (auto at rest, measured px during transitions, 0px when closed)
--accordion-panel-width Panel width (auto at rest, measured px during transitions, 0px when closed)
--radix-accordion-content-height Compatibility alias for Tailwind/Radix accordion keyframes
--radix-accordion-content-width Compatibility alias for width-based integrations

CSS Example

[data-slot="accordion-item"] {
  border-bottom: 1px solid #e5e7eb;
}

[data-slot="accordion-trigger"] {
  width: 100%;
  display: flex;
  align-items: center;
  gap: 1rem;
  padding: 1rem;
  background: transparent;
  border: 0;
  text-align: left;
}

[data-slot="accordion-trigger-icon"] {
  margin-left: auto;
  width: 1rem;
  height: 1rem;
  color: #6b7280;
  transition: transform 0.2s ease;
}

[data-slot="accordion-item"][data-state="open"] [data-slot="accordion-trigger-icon"] {
  transform: rotate(45deg);
}

[data-slot="accordion-content"] {
  overflow: hidden;
  height: var(--accordion-panel-height);
  transition: height 0.2s ease;
}

[data-slot="accordion-content"][data-starting-style] {
  height: 0;
}

[data-slot="accordion-content-inner"] {
  padding: 0 1rem 1rem;
}

Tailwind Example

<div data-slot="accordion" class="overflow-hidden border">
  <div
    data-slot="accordion-item"
    data-value="one"
    class="group border-b data-[open]:bg-muted/50"
  >
    <button
      data-slot="accordion-trigger"
      class="flex w-full items-center gap-6 p-4 text-left text-sm font-medium hover:underline"
    >
      <span>Section One</span>
      <svg
        data-slot="accordion-trigger-icon"
        viewBox="0 0 12 12"
        class="ml-auto size-4 shrink-0 text-muted-foreground transition-transform group-data-[state=open]:rotate-45"
      >
        <path d="M6.75 0H5.25V5.25H0V6.75H5.25V12H6.75V6.75H12V5.25H6.75V0Z" />
      </svg>
    </button>
    <div
      data-slot="accordion-content"
      class="overflow-hidden text-sm data-open:animate-accordion-down data-closed:animate-accordion-up"
    >
      <div
        data-slot="accordion-content-inner"
        class="h-(--accordion-panel-height) px-4 pb-4 pt-0 data-ending-style:h-0 data-starting-style:h-0"
      >
        Content
      </div>
    </div>
  </div>
</div>

Keyboard Navigation

Key Action
Enter / Space Toggle focused item
ArrowDown / ArrowUp Move focus in vertical accordions
ArrowRight / ArrowLeft Move focus in horizontal accordions
Home Move focus to first enabled trigger
End Move focus to last enabled trigger

Disabled items are skipped during roving focus.