Disclosure
Expand/collapse primitive with accessible trigger and animated panel.
Requires Alpine.js
Examples
Default preview
<div class="space-y-4">
{{ partial:components/primitives/disclosure }}
{{ slot:trigger }}
What is included in Kern?
{{ /slot:trigger }}
{{ slot:panel }}
Kern ships copy-pastable primitives, forms, and blocks. Interactive primitives are powered by Alpine.js and
keep behavior accessible by default.
{{ /slot:panel }}
{{ /partial:components/primitives/disclosure }}
{{ partial:components/primitives/disclosure open="true" }}
{{ slot:trigger }}
Is the panel open by default?
{{ /slot:trigger }}
{{ slot:panel }}
Yes — set
{{ partial:docs/components/inline-code class="px-1" }}open="true"{{ /partial:docs/components/inline-code }}
to render expanded on initial load.
{{ /slot:panel }}
{{ /partial:components/primitives/disclosure }}
</div>
<div class="space-y-4">
{{ partial:components/primitives/disclosure }}
{{ slot:trigger }}
What is included in Kern?
{{ /slot:trigger }}
{{ slot:panel }}
Kern ships copy-pastable primitives, forms, and blocks. Interactive primitives are powered by Alpine.js and
keep behavior accessible by default.
{{ /slot:panel }}
{{ /partial:components/primitives/disclosure }}
{{ partial:components/primitives/disclosure open="true" }}
{{ slot:trigger }}
Is the panel open by default?
{{ /slot:trigger }}
{{ slot:panel }}
Yes — set
{{ partial:docs/components/inline-code class="px-1" }}open="true"{{ /partial:docs/components/inline-code }}
to render expanded on initial load.
{{ /slot:panel }}
{{ /partial:components/primitives/disclosure }}
</div>
Props
| Name | Type | Default | Description |
|---|---|---|---|
open
|
boolean
|
false
|
Initial expanded state |
class
|
string
|
|
Additional classes merged via tw_merge (root element only) |
Source
{{#
@name Disclosure
@desc Expand/collapse primitive with accessible trigger and animated panel.
@param open boolean [false] - Initial expanded state
@param class string - Additional classes merged via tw_merge (root element only)
#}}
{{ _is_open = false }}
{{ if open == true || open == 'true'
|| open == 1
|| open == '1' }}
{{ _is_open = true }}
{{ /if }}
{{ _root_classes = 'overflow-hidden rounded-default border border-border bg-background {class}'
| tw_merge }}
<div
class="{{ _root_classes }}"
x-data="{ isOpen: {{ if _is_open }}true{{ else }}false{{ /if }} }"
x-id="['disclosure-trigger', 'disclosure-panel']"
>
<button
type="button"
class="group text-foreground hover:bg-secondary/50 focus-visible:ring-ring focus-visible:ring-offset-background aria-expanded:bg-secondary/60 flex w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
:id="$id('disclosure-trigger')"
:aria-expanded="isOpen"
:aria-controls="$id('disclosure-panel')"
@click="isOpen = !isOpen"
>
<span>
{{ if slot:trigger }}
{{ slot:trigger }}
{{ else }}
Toggle disclosure
{{ /if }}
</span>
<span
class="text-muted-foreground shrink-0 transition-transform duration-200 group-aria-expanded:rotate-180"
aria-hidden="true"
>
{{ svg src="icons/chevron-down" class="size-5" }}
</span>
</button>
<div
x-show="isOpen"
x-collapse
:id="$id('disclosure-panel')"
role="region"
:aria-labelledby="$id('disclosure-trigger')"
style="display: none"
>
<div class="text-muted-foreground px-4 pt-1 pb-4 text-sm leading-relaxed">
{{ if slot:panel }}
{{ slot:panel }}
{{ else }}
{{ slot }}
{{ /if }}
</div>
</div>
</div>
{{#
@name Disclosure
@desc Expand/collapse primitive with accessible trigger and animated panel.
@param open boolean [false] - Initial expanded state
@param class string - Additional classes merged via tw_merge (root element only)
#}}
{{ _is_open = false }}
{{ if open == true || open == 'true'
|| open == 1
|| open == '1' }}
{{ _is_open = true }}
{{ /if }}
{{ _root_classes = 'overflow-hidden rounded-default border border-border bg-background {class}'
| tw_merge }}
<div
class="{{ _root_classes }}"
x-data="{ isOpen: {{ if _is_open }}true{{ else }}false{{ /if }} }"
x-id="['disclosure-trigger', 'disclosure-panel']"
>
<button
type="button"
class="group text-foreground hover:bg-secondary/50 focus-visible:ring-ring focus-visible:ring-offset-background aria-expanded:bg-secondary/60 flex w-full items-center justify-between gap-3 px-4 py-3 text-left text-sm font-medium transition-colors focus-visible:ring-2 focus-visible:ring-offset-2 focus-visible:outline-none"
:id="$id('disclosure-trigger')"
:aria-expanded="isOpen"
:aria-controls="$id('disclosure-panel')"
@click="isOpen = !isOpen"
>
<span>
{{ if slot:trigger }}
{{ slot:trigger }}
{{ else }}
Toggle disclosure
{{ /if }}
</span>
<span
class="text-muted-foreground shrink-0 transition-transform duration-200 group-aria-expanded:rotate-180"
aria-hidden="true"
>
{{ svg src="icons/chevron-down" class="size-5" }}
</span>
</button>
<div
x-show="isOpen"
x-collapse
:id="$id('disclosure-panel')"
role="region"
:aria-labelledby="$id('disclosure-trigger')"
style="display: none"
>
<div class="text-muted-foreground px-4 pt-1 pb-4 text-sm leading-relaxed">
{{ if slot:panel }}
{{ slot:panel }}
{{ else }}
{{ slot }}
{{ /if }}
</div>
</div>
</div>
Dependencies
Packages
marcorieser/tailwind-merge-statamic
composer require marcorieser/tailwind-merge-statamic
composer require marcorieser/tailwind-merge-statamic
@alpinejs/collapse
npm install @alpinejs/collapse
npm install @alpinejs/collapse
Internal dependencies
- Uses: x-id + $id() for unique trigger/panel IDs
- Slots: trigger, panel (default slot also supported)