Collapsible
An interactive component which expands and collapses a section of content.
Loading...
<twig:Collapsible class="flex w-[350px] flex-col gap-2 self-start">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold">Order #4189</h4>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
<span class="sr-only">Toggle details</span>
</twig:Button>
</twig:Collapsible:Trigger>
</div>
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
<span class="text-muted-foreground">Status</span>
<span class="font-medium">Shipped</span>
</div>
<twig:Collapsible:Content class="flex flex-col gap-2">
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">Shipping address</p>
<p class="text-muted-foreground">100 Market St, San Francisco</p>
</div>
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">Items</p>
<p class="text-muted-foreground">2x Studio Headphones</p>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
Installation
bin/console ux:install collapsible --kit shadcn
That's it!
Install the following Composer dependencies:
composer require tales-from-a-dev/twig-tailwind-extra:^1.0.0
Copy the following file(s) into your Symfony app:
assets/controllers/collapsible_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['trigger', 'content'];
static values = {
open: { type: Boolean, default: false },
};
connect() {
this.updateState();
}
toggle() {
this.openValue = !this.openValue;
}
openValueChanged() {
this.updateState();
}
updateState() {
const open = this.openValue;
const state = open ? 'open' : 'closed';
this.element.dataset.state = state;
for (const trigger of this.triggerTargets) {
trigger.setAttribute('aria-expanded', String(open));
trigger.dataset.state = state;
}
for (const content of this.contentTargets) {
content.dataset.state = state;
content.setAttribute('aria-hidden', String(!open));
if (open) {
content.removeAttribute('hidden');
} else {
content.setAttribute('hidden', '');
}
}
}
}
templates/components/Collapsible.html.twig
{# @prop open boolean Whether the collapsible is open by default. Defaults to `false` #}
{# @block content The collapsible content, typically a `Collapsible:Trigger` and a `Collapsible:Content` #}
{%- props open = false -%}
{%- set _collapsible_open = open -%}
<div
class="{{ ('group ' ~ attributes.render('class'))|tailwind_merge }}"
{{ attributes.defaults({
'data-slot': 'collapsible',
'data-controller': 'collapsible',
'data-collapsible-open-value': open ? 'true' : 'false',
'data-state': open ? 'open' : 'closed',
}) }}
>
{%- block content %}{% endblock -%}
</div>
templates/components/Collapsible/Content.html.twig
{# @block content The content revealed when the collapsible is open #}
<div{{ attributes.defaults({
'data-slot': 'collapsible-content',
'data-collapsible-target': 'content',
'aria-hidden': not _collapsible_open ? 'true' : 'false',
'data-state': _collapsible_open ? 'open' : 'closed',
...(not _collapsible_open ? {hidden: ''} : {}),
}) }}
>
{%- block content %}{% endblock -%}
</div>
templates/components/Collapsible/Trigger.html.twig
{# @block content The clickable element that toggles the collapsible (e.g., a `Button`) #}
{%- set collapsible_trigger_attrs = {
'data-slot': 'collapsible-trigger',
'data-collapsible-target': 'trigger',
'data-action': 'click->collapsible#toggle'|html_attr_type('sst'),
'aria-expanded': _collapsible_open ? 'true' : 'false',
'data-state': _collapsible_open ? 'open' : 'closed',
} -%}
{%- block content %}{% endblock -%}
Happy coding!
Usage
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" {{ ...collapsible_trigger_attrs }}>Show details</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
The content is now visible.
</twig:Collapsible:Content>
</twig:Collapsible>
Examples
Basic
Loading...
<twig:Card class="mx-auto w-full max-w-sm self-start">
<twig:Card:Content class="p-3">
<twig:Collapsible class="rounded-md data-[state=open]:bg-muted">
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full" {{ ...collapsible_trigger_attrs }}>
Product details
<twig:ux:icon name="lucide:chevron-down" class="ml-auto size-4 transition-transform group-data-[state=open]:rotate-180" />
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="flex flex-col items-start gap-2 p-2.5 pt-0 text-sm">
<div>
This panel can be expanded or collapsed to reveal additional content.
</div>
<twig:Button size="xs">Learn More</twig:Button>
</twig:Collapsible:Content>
</twig:Collapsible>
</twig:Card:Content>
</twig:Card>
Settings Panel
Use a trigger button to reveal additional settings.
Loading...
<twig:Card class="mx-auto w-full max-w-xs">
<twig:Card:Header>
<twig:Card:Title>Radius</twig:Card:Title>
<twig:Card:Description>Set the corner radius of the element.</twig:Card:Description>
</twig:Card:Header>
<twig:Card:Content>
<twig:Collapsible class="flex items-start gap-2">
<div class="w-full space-y-2">
<div class="grid grid-cols-2 gap-2">
<twig:Field>
<twig:Field:Label for="radius-1" class="sr-only">Radius X</twig:Field:Label>
<twig:Input id="radius-1" placeholder="0" value="0" />
</twig:Field>
<twig:Field>
<twig:Field:Label for="radius-2" class="sr-only">Radius Y</twig:Field:Label>
<twig:Input id="radius-2" placeholder="0" value="0" />
</twig:Field>
</div>
<twig:Collapsible:Content>
<div class="grid grid-cols-2 gap-2">
<twig:Field>
<twig:Field:Label for="radius-3" class="sr-only">Radius X</twig:Field:Label>
<twig:Input id="radius-3" placeholder="0" value="0" />
</twig:Field>
<twig:Field>
<twig:Field:Label for="radius-4" class="sr-only">Radius Y</twig:Field:Label>
<twig:Input id="radius-4" placeholder="0" value="0" />
</twig:Field>
</div>
</twig:Collapsible:Content>
</div>
<twig:Collapsible:Trigger>
<twig:Button variant="outline" size="icon" class="group" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:maximize" class="size-4 group-data-[state=open]:hidden" />
<twig:ux:icon name="lucide:minimize" class="size-4 hidden group-data-[state=open]:block" />
</twig:Button>
</twig:Collapsible:Trigger>
</twig:Collapsible>
</twig:Card:Content>
</twig:Card>
File Tree
Use nested collapsibles to build a file tree.
Loading...
<twig:Card class="mx-auto w-full max-w-sm">
<twig:Card:Header class="p-2.5">
<div class="flex items-center gap-1.5 text-sm font-medium">
<twig:ux:icon name="lucide:panel-left" class="size-4" />
Explorer
</div>
</twig:Card:Header>
<twig:Card:Content class="p-2.5 pt-0">
<div class="flex flex-col gap-0.5">
{# Folder: components #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
components
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
{# Nested folder: components/ui #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
ui
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
button.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
card.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
dialog.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
input.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
select.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
table.tsx
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
login-form.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
register-form.tsx
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: lib #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
lib
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
utils.ts
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
cn.ts
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
api.ts
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: hooks #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
hooks
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
use-media-query.ts
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
use-debounce.ts
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
use-local-storage.ts
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: types #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
types
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
index.d.ts
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
api.d.ts
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: public #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" class="group w-full justify-start gap-1.5 h-7 px-2 text-sm" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="size-3.5 transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" class="size-3.5 text-muted-foreground" />
public
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
<div class="ml-4 flex flex-col gap-0.5">
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
favicon.ico
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
logo.svg
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
images
</twig:Button>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Root files #}
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
app.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
layout.tsx
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
globals.css
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
package.json
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
tsconfig.json
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
README.md
</twig:Button>
<twig:Button variant="ghost" class="w-full justify-start gap-1.5 h-7 px-2 text-sm">
<twig:ux:icon name="lucide:file" class="size-3.5 text-muted-foreground" />
.gitignore
</twig:Button>
</div>
</twig:Card:Content>
</twig:Card>
RTL
Loading...
<div class="flex w-[350px] flex-col gap-4">
{# Arabic #}
<twig:Collapsible class="flex flex-col gap-2" dir="rtl">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold">الطلب #4189</h4>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
<span class="sr-only">Toggle details</span>
</twig:Button>
</twig:Collapsible:Trigger>
</div>
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
<span class="text-muted-foreground">الحالة</span>
<span class="font-medium">تم الشحن</span>
</div>
<twig:Collapsible:Content class="flex flex-col gap-2">
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">عنوان الشحن</p>
<p class="text-muted-foreground">100 Market St, San Francisco</p>
</div>
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">العناصر</p>
<p class="text-muted-foreground">2x سماعات الاستوديو</p>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# English #}
<twig:Collapsible class="flex flex-col gap-2" dir="ltr">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold">Order #4189</h4>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
<span class="sr-only">Toggle details</span>
</twig:Button>
</twig:Collapsible:Trigger>
</div>
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
<span class="text-muted-foreground">Status</span>
<span class="font-medium">Shipped</span>
</div>
<twig:Collapsible:Content class="flex flex-col gap-2">
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">Shipping address</p>
<p class="text-muted-foreground">100 Market St, San Francisco</p>
</div>
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">Items</p>
<p class="text-muted-foreground">2x Studio Headphones</p>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Hebrew #}
<twig:Collapsible class="flex flex-col gap-2" dir="rtl">
<div class="flex items-center justify-between gap-4 px-4">
<h4 class="text-sm font-semibold">הזמנה #4189</h4>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="icon" class="size-8" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevrons-up-down" class="size-4" />
<span class="sr-only">Toggle details</span>
</twig:Button>
</twig:Collapsible:Trigger>
</div>
<div class="flex items-center justify-between rounded-md border px-4 py-2 text-sm">
<span class="text-muted-foreground">סטטוס</span>
<span class="font-medium">נשלח</span>
</div>
<twig:Collapsible:Content class="flex flex-col gap-2">
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">כתובת משלוח</p>
<p class="text-muted-foreground">100 Market St, San Francisco</p>
</div>
<div class="rounded-md border px-4 py-2 text-sm">
<p class="font-medium">פריטים</p>
<p class="text-muted-foreground">2x אוזניות סטודיו</p>
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
</div>
API Reference
Collapsible
| Prop | Type | Description |
|---|---|---|
open |
boolean |
Whether the collapsible is open by default. Defaults to false |
| Block | Description |
|---|---|
content |
The collapsible content, typically a Collapsible:Trigger and a Collapsible:Content |
Collapsible:Content
| Block | Description |
|---|---|
content |
The content revealed when the collapsible is open |
Collapsible:Trigger
| Block | Description |
|---|---|
content |
The clickable element that toggles the collapsible (e.g., a Button) |