disclosure
Accordion
Compound disclosure sections with single or multiple open items, ARIA expanded/controls pairing, and motion-safe height transitions.
Copied!
Examples
Single type
Only one panel open at a time.
Copied!
Ships within 2 business days across Indonesia.
Start a return within 14 days of delivery.
Email support replies within one business day.
<x-ui.accordion type="single" default="shipping">
<x-ui.accordion.item id="shipping" title="Shipping">
Ships within 2 business days across Indonesia.
</x-ui.accordion.item>
<x-ui.accordion.item id="returns" title="Returns">
Start a return within 14 days of delivery.
</x-ui.accordion.item>
<x-ui.accordion.item id="support" title="Support">
Email support replies within one business day.
</x-ui.accordion.item>
</x-ui.accordion>
<div class="divide-y divide-border rounded-card border border-border bg-surface-raised" x-data="{ openId: 'shipping', toggle(id) { this.openId = this.openId === id ? null : id }, isOpen(id) { return this.openId === id } }">
<div class="px-4">
<h3 class="m-0">
<button type="button" class="flex w-full items-center justify-between py-4 text-left text-sm font-medium text-fg" x-on:click="toggle('shipping')" x-bind:aria-expanded="isOpen('shipping').toString()" aria-controls="shipping-panel" id="shipping-header">
<span>Shipping</span>
</button>
</h3>
<div id="shipping-panel" role="region" aria-labelledby="shipping-header" class="grid transition-[grid-template-rows] duration-200" x-bind:class="isOpen('shipping') ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'">
<div class="overflow-hidden">
<p class="pb-4 text-sm text-fg-muted">Ships within 2 business days across Indonesia.</p>
</div>
</div>
</div>
<div class="px-4">
<h3 class="m-0">
<button type="button" class="flex w-full items-center justify-between py-4 text-left text-sm font-medium text-fg" x-on:click="toggle('returns')" x-bind:aria-expanded="isOpen('returns').toString()" aria-controls="returns-panel" id="returns-header">
<span>Returns</span>
</button>
</h3>
<div id="returns-panel" role="region" aria-labelledby="returns-header" class="grid transition-[grid-template-rows] duration-200" x-bind:class="isOpen('returns') ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'">
<div class="overflow-hidden">
<p class="pb-4 text-sm text-fg-muted">Start a return within 14 days of delivery.</p>
</div>
</div>
</div>
</div>
Multiple type
Several panels can stay open together.
Copied!
Generate personal access tokens from Settings.
Subscribe to deploy and billing events.
Free workspaces share a 60 request per minute budget.
<x-ui.accordion type="multiple" :default="['api', 'webhooks']">
<x-ui.accordion.item id="api" title="API access">
Generate personal access tokens from Settings.
</x-ui.accordion.item>
<x-ui.accordion.item id="webhooks" title="Webhooks">
Subscribe to deploy and billing events.
</x-ui.accordion.item>
<x-ui.accordion.item id="limits" title="Rate limits">
Free workspaces share a 60 request per minute budget.
</x-ui.accordion.item>
</x-ui.accordion>
<div class="divide-y divide-border rounded-card border border-border bg-surface-raised" x-data="{ openIds: ['api', 'webhooks'], toggle(id) { this.openIds = this.openIds.includes(id) ? this.openIds.filter(v => v !== id) : [...this.openIds, id] }, isOpen(id) { return this.openIds.includes(id) } }">
<div class="px-4">
<h3 class="m-0">
<button type="button" id="api-header" class="flex w-full items-center justify-between py-4 text-left text-sm font-medium text-fg" x-on:click="toggle('api')" x-bind:aria-expanded="isOpen('api').toString()" aria-controls="api-panel">
<span>API access</span>
</button>
</h3>
<div id="api-panel" role="region" aria-labelledby="api-header" class="grid transition-[grid-template-rows] duration-200" x-bind:class="isOpen('api') ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'">
<div class="overflow-hidden">
<p class="pb-4 text-sm text-fg-muted">Generate personal access tokens from Settings.</p>
</div>
</div>
</div>
<div class="px-4">
<h3 class="m-0">
<button type="button" id="webhooks-header" class="flex w-full items-center justify-between py-4 text-left text-sm font-medium text-fg" x-on:click="toggle('webhooks')" x-bind:aria-expanded="isOpen('webhooks').toString()" aria-controls="webhooks-panel">
<span>Webhooks</span>
</button>
</h3>
<div id="webhooks-panel" role="region" aria-labelledby="webhooks-header" class="grid transition-[grid-template-rows] duration-200" x-bind:class="isOpen('webhooks') ? 'grid-rows-[1fr]' : 'grid-rows-[0fr]'">
<div class="overflow-hidden">
<p class="pb-4 text-sm text-fg-muted">Subscribe to deploy and billing events.</p>
</div>
</div>
</div>
</div>
Props API
| Prop | Type | Default | Description |
|---|---|---|---|
| type | single|multiple | single | single keeps one panel open. multiple allows several panels open together. |
| default | string|array|null | null | Initially open item id, or list of ids when type is multiple. |
| item.id | string | required | Stable id for the accordion item. |
| item.title | string | required | Header button label. |
Accessibility notes
Architected with WCAG 2.2 AA patterns in mind. These notes document the exact keyboard and ARIA behavior for this component.
- Header buttons expose x-bind:aria-expanded and aria-controls pointing at the panel id.
- Panels use role="region" and aria-labelledby pointing back at the header button.
- Keyboard: Tab moves between headers. Enter or Space toggles the focused header (native button behavior).
- Height and opacity transitions respect prefers-reduced-motion via motion-reduce utilities and global reduced-motion overrides.
Built by Praesepe
Architecture & custom software partner
Praesepe uses these exact UI primitives to ship SME dashboards, inventory systems, invoicing desks, and internal tools faster, without rebuilding a design system for every client.
- Same Blade + Tailwind 4 + Alpine 3 stack you see in this docs site
- Opinionated for Indonesian SME ops: invoices, stock, roles, approvals
- Architected with WCAG 2.2 AA patterns in mind for keyboard and ARIA