Collapsible
An interactive component which expands/collapses a panel.
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
Note
Available since UX Toolkit 3.0.
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 symfony/ux-twig-component:^3.1
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 -%}
{%- do provide('collapsible.open', open) -%}
<div
{{ 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 #}
{%- set _collapsible_open = inject('collapsible.open', false) -%}
<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_open = inject('collapsible.open', false) -%}
{%- 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 }}>Can I use this in my project?</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content>
Yes. Free to use for personal and commercial projects. No attribution required.
</twig:Collapsible:Content>
</twig:Collapsible>
Examples
Basic
Loading...
<twig:Card class="mx-auto w-full max-w-sm self-start">
<twig:Card:Content>
<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 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 self-start" size="sm">
<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="grid w-full 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>
<twig:Collapsible:Content class="col-span-full grid grid-cols-subgrid 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>
</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-[16rem] gap-2 self-start" size="sm">
<twig:Card:Header>
<twig:Tabs defaultValue="explorer">
<twig:Tabs:List class="w-full">
<twig:Tabs:Trigger value="explorer">Explorer</twig:Tabs:Trigger>
<twig:Tabs:Trigger value="outline">Outline</twig:Tabs:Trigger>
</twig:Tabs:List>
</twig:Tabs>
</twig:Card:Header>
<twig:Card:Content>
<div class="flex flex-col gap-1">
{# Folder: components #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
components
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{# Nested folder: components/ui #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
ui
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{% for file in ['button.tsx', 'card.tsx', 'dialog.tsx', 'input.tsx', 'select.tsx', 'table.tsx'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{% for file in ['login-form.tsx', 'register-form.tsx'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: lib #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
lib
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{% for file in ['utils.ts', 'cn.ts', 'api.ts'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: hooks #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
hooks
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{% for file in ['use-media-query.ts', 'use-debounce.ts', 'use-local-storage.ts'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: types #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
types
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{% for file in ['index.d.ts', 'api.d.ts'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Folder: public #}
<twig:Collapsible>
<twig:Collapsible:Trigger>
<twig:Button variant="ghost" size="sm" class="group w-full justify-start transition-none hover:bg-accent hover:text-accent-foreground" {{ ...collapsible_trigger_attrs }}>
<twig:ux:icon name="lucide:chevron-right" class="transition-transform group-data-[state=open]:rotate-90" />
<twig:ux:icon name="lucide:folder" />
public
</twig:Button>
</twig:Collapsible:Trigger>
<twig:Collapsible:Content class="mt-1 ml-5">
<div class="flex flex-col gap-1">
{% for file in ['favicon.ico', 'logo.svg', 'images'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Collapsible:Content>
</twig:Collapsible>
{# Root files #}
{% for file in ['app.tsx', 'layout.tsx', 'globals.css', 'package.json', 'tsconfig.json', 'README.md', '.gitignore'] %}
<twig:Button variant="link" size="sm" class="w-full justify-start gap-2 text-foreground">
<twig:ux:icon name="lucide:file" />
<span>{{ file }}</span>
</twig:Button>
{% endfor %}
</div>
</twig:Card:Content>
</twig:Card>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
Loading...
<div class="flex w-[350px] flex-col gap-4 self-start">
{# 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>
{# 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
Component 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 |
Component Collapsible:Content
| Block | Description |
|---|---|
content |
The content revealed when the collapsible is open |
Component Collapsible:Trigger
| Block | Description |
|---|---|
content |
The clickable element that toggles the collapsible (e.g., a Button) |