View as Markdown
Drawer
A panel that slides in from the edge of the screen to display supplementary content.
100%
Loading...
<div style="min-height: 400px">
<twig:Drawer id="drawer-demo">
<twig:Drawer:Trigger>
<twig:Button variant="outline" {{ ...drawer_trigger_attrs }}>Open Drawer</twig:Button>
</twig:Drawer:Trigger>
<twig:Drawer:Content>
<div class="mx-auto w-full max-w-sm">
<twig:Drawer:Header>
<twig:Drawer:Title>Move Goal</twig:Drawer:Title>
<twig:Drawer:Description>Set your daily activity goal.</twig:Drawer:Description>
</twig:Drawer:Header>
<div class="p-4 pb-0">
<p class="text-muted-foreground text-sm">Adjust the target you want to reach each day.</p>
</div>
<twig:Drawer:Footer>
<twig:Button>Submit</twig:Button>
<twig:Drawer:Close>
<twig:Button variant="outline" {{ ...drawer_close_attrs }}>Cancel</twig:Button>
</twig:Drawer:Close>
</twig:Drawer:Footer>
</div>
</twig:Drawer:Content>
</twig:Drawer>
</div>
Installation
Note
Available since UX Toolkit 3.5.
php bin/console ux:install drawer --kit shadcn
Install the following Composer dependencies:
composer require twig/extra-bundle twig/html-extra:^3.24.0 tales-from-a-dev/twig-tailwind-extra:^1.3.0 symfony/ux-twig-component:^3.5 symfony/ux-icons
Install the following front-end dependencies:
npm install shadcn tw-animate-css
# ...or with Symfony AssetMapper
php bin/console importmap:require tw-animate-css/dist/tw-animate.css shadcn/dist/tailwind.css
Copy the following file(s) into your app:
templates/components/Drawer.html.twig
{%- props
## string Unique identifier used to generate internal Drawer IDs.
id,
## 'top'|'right'|'bottom'|'left' Which edge the drawer slides in from.
direction = 'bottom',
## boolean Whether the drawer is open on initial render.
open = false
-%}
{%- set _drawer_id = 'drawer-' ~ id -%}
{%- set _drawer_title_id = _drawer_id ~ '-title' -%}
{%- set _drawer_description_id = _drawer_id ~ '-description' -%}
{%- do provide('drawer.id', _drawer_id) -%}
{%- do provide('drawer.direction', direction) -%}
{%- do provide('drawer.titleId', _drawer_title_id) -%}
{%- do provide('drawer.descriptionId', _drawer_description_id) -%}
<div
data-slot="drawer"
data-dialog-open-value="{{ open ? 'true' : 'false' }}"
{{ attributes.defaults({
'data-controller': 'dialog',
}) }}
>
{## The drawer structure, typically includes `Drawer:Trigger` and `Drawer:Content`. -#}
{% block content %}{% endblock %}
</div>
templates/components/Drawer/Close.html.twig
{%- set drawer_close_attrs = {
'data-slot': 'drawer-close',
'data-action': 'click->dialog#close'|html_attr_type('sst'),
} -%}
{##- The close trigger element (e.g., a `Button`) that closes the drawer when clicked. -#}
{%- block content %}{% endblock -%}
templates/components/Drawer/Content.html.twig
{%- set _drawer_id = inject('drawer.id') -%}
{%- set _drawer_titleId = inject('drawer.titleId') -%}
{%- set _drawer_descriptionId = inject('drawer.descriptionId') -%}
{%- set _drawer_direction = inject('drawer.direction', 'bottom') -%}
{%- set style = html_cva(
base: 'group/drawer-content fixed z-50 m-0 open:flex h-auto max-h-none w-auto max-w-none flex-col bg-popover p-0 text-sm text-popover-foreground outline-none transition duration-300 ease-in-out backdrop:bg-black/10 supports-backdrop-filter:backdrop:backdrop-blur-xs backdrop:opacity-0 backdrop:transition-opacity backdrop:duration-300 backdrop:ease-in-out open:backdrop:opacity-100 starting:open:backdrop:opacity-0 data-closing:backdrop:opacity-0',
variants: {
direction: {
top: 'inset-x-0 top-0 bottom-auto mb-24 max-h-[80vh] rounded-b-xl border-b -translate-y-full open:translate-y-0 starting:open:-translate-y-full data-closing:-translate-y-full',
bottom: 'inset-x-0 bottom-0 top-auto mt-24 max-h-[80vh] rounded-t-xl border-t translate-y-full open:translate-y-0 starting:open:translate-y-full data-closing:translate-y-full',
right: 'inset-y-0 right-0 left-auto h-full w-3/4 rounded-l-xl border-l translate-x-full open:translate-x-0 starting:open:translate-x-full data-closing:translate-x-full sm:max-w-sm',
left: 'inset-y-0 left-0 right-auto h-full w-3/4 rounded-r-xl border-r -translate-x-full open:translate-x-0 starting:open:-translate-x-full data-closing:-translate-x-full sm:max-w-sm',
},
},
) -%}
<dialog
id="{{ _drawer_id }}"
aria-labelledby="{{ _drawer_titleId }}"
aria-describedby="{{ _drawer_descriptionId }}"
data-slot="drawer-content"
data-dialog-target="dialog"
data-direction="{{ _drawer_direction }}"
{{ attributes.defaults({
class: style.apply({direction: _drawer_direction})|tailwind_classes,
'data-action': 'keydown.esc->dialog#close:prevent click->dialog#closeOnClickOutside',
}) }}
>
{% if _drawer_direction == 'bottom' %}
<div class="mx-auto mt-4 h-1 w-[100px] shrink-0 rounded-full bg-muted"></div>
{% endif %}
{##- The drawer content, typically includes `Drawer:Header` and optionally `Drawer:Footer`. -#}
{%- block content %}{% endblock -%}
</dialog>
templates/components/Drawer/Description.html.twig
{%- set _drawer_descriptionId = inject('drawer.descriptionId') -%}
<p
id="{{ _drawer_descriptionId }}"
data-slot="drawer-description"
{{ attributes.without('id').defaults({class: 'text-muted-foreground text-sm'|tailwind_classes}) }}
>
{##- The descriptive text explaining the drawer purpose. -#}
{%- block content %}{% endblock -%}
</p>
templates/components/Drawer/Footer.html.twig
<div
data-slot="drawer-footer"
{{ attributes.defaults({
class: 'mt-auto flex flex-col gap-2 p-4'|tailwind_classes,
}) }}
>
{##- The footer area, typically contains action buttons. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Drawer/Header.html.twig
<div
data-slot="drawer-header"
{{ attributes.defaults({
class: 'flex flex-col gap-0.5 p-4 group-data-[direction=bottom]/drawer-content:text-center group-data-[direction=top]/drawer-content:text-center md:gap-0.5 md:text-start'|tailwind_classes,
}) }}
>
{##- The header area, typically contains `Drawer:Title` and `Drawer:Description`. -#}
{%- block content %}{% endblock -%}
</div>
templates/components/Drawer/Title.html.twig
{%- set _drawer_titleId = inject('drawer.titleId') -%}
<h2
id="{{ _drawer_titleId }}"
data-slot="drawer-title"
{{ attributes.without('id').defaults({class: 'text-base font-medium text-foreground'|tailwind_classes}) }}
>
{##- The title text of the drawer. -#}
{%- block content %}{% endblock -%}
</h2>
templates/components/Drawer/Trigger.html.twig
{%- set drawer_trigger_attrs = {
'data-slot': 'drawer-trigger',
'data-action': 'click->dialog#open'|html_attr_type('sst'),
'data-dialog-target': 'trigger',
'aria-haspopup': 'dialog',
} -%}
{##- The trigger element (e.g., a `Button`) that opens the drawer when clicked. -#}
{%- block content %}{% endblock -%}
assets/controllers/dialog_controller.js
import { Controller } from '@hotwired/stimulus';
export default class extends Controller {
static targets = ['trigger', 'dialog'];
static values = {
open: Boolean,
};
#wasOpen = null;
#closing = null;
connect() {
if (this.#wasOpen ?? this.openValue) {
this.open();
}
}
disconnect() {
// A <dialog> taken out of the DOM comes back open but no longer modal, so reopen it on reconnect.
this.#wasOpen = this.dialogTarget.open && !this.dialogTarget.hasAttribute('data-closing');
this.#closing = null;
this.dialogTarget.removeAttribute('data-closing');
if (this.dialogTarget.open) {
this.dialogTarget.close();
}
}
open() {
this.#closing = null;
// Reopened before the exit transition ended: transition back instead.
const reopened = this.dialogTarget.open && this.dialogTarget.hasAttribute('data-closing');
this.dialogTarget.removeAttribute('data-closing');
if (!reopened) {
this.dialogTarget.showModal();
}
this._focusInitialElement();
if (this.hasTriggerTarget) {
this.triggerTarget.setAttribute('aria-expanded', 'true');
}
}
closeOnClickOutside({ target }) {
if (target === this.dialogTarget) {
this.close();
}
}
_focusInitialElement() {
// showModal() already focuses an [autofocus] target or the first focusable element;
// when the author did not opt into autofocus, prefer the first form field instead.
if (this.dialogTarget.querySelector('[autofocus]')) {
return;
}
const field = this.dialogTarget.querySelector(
'input:not([type="hidden"]):not([disabled]), textarea:not([disabled]), select:not([disabled])'
);
field?.focus();
}
close() {
const dialog = this.dialogTarget;
if (!dialog.open || dialog.hasAttribute('data-closing')) {
return;
}
if (this.hasTriggerTarget) {
this.triggerTarget.setAttribute('aria-expanded', 'false');
}
// Only Chromium can transition a <dialog> out of the top layer, so play the exit
// transition while it is still open, through [data-closing], then close it.
dialog.setAttribute('data-closing', '');
const closing = {};
this.#closing = closing;
const finish = () => this._finishClosing(dialog, closing);
// The <dialog> and its ::backdrop only: an endless animation in the content would never let it close.
const animations = dialog
.getAnimations({ subtree: true })
.filter(
(animation) =>
animation.effect?.target === dialog && Number.isFinite(animation.effect.getComputedTiming().endTime)
);
if (animations.length === 0) {
finish();
return;
}
Promise.allSettled(animations.map((animation) => animation.finished)).then(finish);
// A paused animation never finishes: close anyway once the longest one should have ended.
setTimeout(
finish,
Math.max(...animations.map((animation) => animation.effect.getComputedTiming().endTime)) + 100
);
}
_finishClosing(dialog, closing) {
// open() or disconnect() ran in the meantime and cancelled this closing.
if (this.#closing !== closing) {
return;
}
this.#closing = null;
dialog.removeAttribute('data-closing');
dialog.close();
}
}
templates/components/Dialog.html.twig
{%- props
## string Unique identifier used to generate internal Dialog IDs.
id,
## boolean Whether the dialog is open on initial render.
open = false
-%}
{%- set _dialog_id = 'dialog-' ~ id -%}
{%- set _dialog_title_id = _dialog_id ~ '-title' -%}
{%- set _dialog_description_id = _dialog_id ~ '-description' -%}
{%- do provide('dialog.id', _dialog_id) -%}
{%- do provide('dialog.titleId', _dialog_title_id) -%}
{%- do provide('dialog.descriptionId', _dialog_description_id) -%}
<div
data-slot="dialog"
data-dialog-open-value="{{ open ? 'true' : 'false' }}"
{{ attributes.defaults({
'data-controller': 'dialog',
}) }}
>
{## The dialog structure, typically includes `Dialog:Trigger` and `Dialog:Content`. -#}
{% block content %}{% endblock %}
</div>
templates/components/Dialog/Close.html.twig
{%- set dialog_close_attrs = {
'data-slot': 'dialog-close',
'data-action': 'click->dialog#close'|html_attr_type('sst'),
} -%}
{##- The close trigger element (e.g., a `Button`) that closes the dialog when clicked. -#}
{%- block content %}{% endblock -%}
templates/components/Dialog/Content.html.twig
{%- props
## boolean Whether to display the close button in the top-right corner.
showCloseButton = true
-%}
{%- set _dialog_id = inject('dialog.id') -%}
{%- set _dialog_titleId = inject('dialog.titleId') -%}
{%- set _dialog_descriptionId = inject('dialog.descriptionId') -%}
<dialog
id="{{ _dialog_id }}"
aria-labelledby="{{ _dialog_titleId }}"
aria-describedby="{{ _dialog_descriptionId }}"
data-slot="dialog-content"
data-dialog-target="dialog"
{{ attributes.defaults({
class: 'fixed top-1/2 left-1/2 z-50 w-full max-w-[calc(100%-2rem)] -translate-x-1/2 -translate-y-1/2 gap-4 rounded-xl bg-popover p-4 text-sm text-popover-foreground ring-1 ring-foreground/10 outline-none sm:max-w-sm opacity-0 scale-95 transition-all duration-100 backdrop:duration-100 open:grid open:scale-100 open:opacity-100 open:backdrop:bg-black/10 supports-backdrop-filter:open:backdrop:backdrop-blur-xs starting:open:scale-95 starting:open:opacity-0 data-closing:scale-95 data-closing:opacity-0 data-closing:backdrop:bg-transparent supports-backdrop-filter:data-closing:backdrop:backdrop-blur-none'|tailwind_classes,
'data-action': 'keydown.esc->dialog#close:prevent click->dialog#closeOnClickOutside',
}) }}
>
{##- The dialog content, typically includes `Dialog:Header` and optionally `Dialog:Footer`. -#}
{%- block content %}{% endblock -%}
{% if showCloseButton %}
<twig:Button
type="button"
variant="ghost"
size="icon-sm"
class="absolute top-2 end-2"
data-slot="dialog-close"
data-action="click->dialog#close"
>
<twig:ux:icon name="lucide:x" />
<span class="sr-only">Close</span>
</twig:Button>
{% endif %}
</dialog>
templates/components/Dialog/Description.html.twig
{%- set _dialog_descriptionId = inject('dialog.descriptionId') -%}
<p
id="{{ _dialog_descriptionId }}"
data-slot="dialog-description"
{{ attributes.without('id').defaults({class: 'text-muted-foreground text-sm *:[a]:underline *:[a]:underline-offset-3 *:[a]:hover:text-foreground'|tailwind_classes}) }}
>
{##- The descriptive text explaining the dialog purpose. -#}
{%- block content %}{% endblock -%}
</p>
templates/components/Dialog/Footer.html.twig
<footer
data-slot="dialog-footer"
{{ attributes.defaults({
class: '-mx-4 -mb-4 flex flex-col-reverse gap-2 rounded-b-xl border-t bg-muted/50 p-4 sm:flex-row sm:justify-end'|tailwind_classes,
}) }}
>
{##- The footer area, typically contains action buttons. -#}
{%- block content %}{% endblock -%}
</footer>
templates/components/Dialog/Header.html.twig
<header
data-slot="dialog-header"
{{ attributes.defaults({
class: 'flex flex-col gap-2'|tailwind_classes,
}) }}
>
{##- The header area, typically contains `Dialog:Title` and `Dialog:Description`. -#}
{%- block content %}{% endblock -%}
</header>
templates/components/Dialog/Title.html.twig
{%- set _dialog_titleId = inject('dialog.titleId') -%}
<h2
id="{{ _dialog_titleId }}"
data-slot="dialog-title"
{{ attributes.without('id').defaults({class: 'cn-font-heading text-base leading-none font-medium'|tailwind_classes}) }}
>
{##- The title text of the dialog. -#}
{%- block content %}{% endblock -%}
</h2>
templates/components/Dialog/Trigger.html.twig
{%- set dialog_trigger_attrs = {
'data-slot': 'dialog-trigger',
'data-action': 'click->dialog#open'|html_attr_type('sst'),
'data-dialog-target': 'trigger',
'aria-haspopup': 'dialog',
} -%}
{##- The trigger element (e.g., a `Button`) that opens the dialog when clicked. -#}
{%- block content %}{% endblock -%}
templates/components/Button.html.twig
{%- props
## 'default'|'secondary'|'destructive'|'outline'|'ghost'|'link' The visual style variant.
variant = 'default',
## 'default'|'xs'|'sm'|'lg'|'icon'|'icon-xs'|'icon-sm'|'icon-lg' The button size.
size = 'default',
## 'button'|'a' The HTML tag to render; use `a` when the button navigates.
as = 'button'
-%}
{%- set style = html_cva(
base: "group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
variants: {
variant: {
default: 'bg-primary text-primary-foreground hover:bg-primary/80',
outline: 'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
secondary: 'bg-secondary text-secondary-foreground hover:bg-[color-mix(in_oklch,var(--secondary),var(--foreground)_5%)] aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
ghost: 'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
destructive: 'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
link: 'text-primary underline-offset-4 hover:underline',
},
size: {
default: 'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3",
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pe-1.5 has-data-[icon=inline-start]:ps-1.5 [&_svg:not([class*='size-'])]:size-3.5",
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pe-2 has-data-[icon=inline-start]:ps-2',
icon: 'size-8',
'icon-xs': "size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
'icon-sm': 'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
'icon-lg': 'size-9',
},
},
) -%}
<{{ as }}
data-slot="{{ attributes.render('data-slot')|default('button') }}"
data-variant="{{ variant }}"
data-size="{{ size }}"
{{ attributes.defaults({
class: style.apply({variant: variant, size: size})|tailwind_classes,
type: as == 'button' ? 'button' : false,
}) }}
>
{##- The button label and/or icon. -#}
{%- block content %}{% endblock -%}
</{{ as }}>
Usage
<twig:Drawer id="drawer">
<twig:Drawer:Trigger>
<twig:Button variant="outline" {{ ...drawer_trigger_attrs }}>Open</twig:Button>
</twig:Drawer:Trigger>
<twig:Drawer:Content>
<twig:Drawer:Header>
<twig:Drawer:Title>Are you absolutely sure?</twig:Drawer:Title>
<twig:Drawer:Description>This action cannot be undone.</twig:Drawer:Description>
</twig:Drawer:Header>
<twig:Drawer:Footer>
<twig:Button>Submit</twig:Button>
<twig:Drawer:Close>
<twig:Button variant="outline" {{ ...drawer_close_attrs }}>Cancel</twig:Button>
</twig:Drawer:Close>
</twig:Drawer:Footer>
</twig:Drawer:Content>
</twig:Drawer>
Examples
Directions
Use the direction prop to set the edge the drawer slides in from: top, right, bottom (default) or left.
100%
Loading...
<div class="flex flex-wrap gap-2" style="min-height: 400px">
{% for direction in ['top', 'right', 'bottom', 'left'] %}
<twig:Drawer id="drawer-direction-{{ direction }}" direction="{{ direction }}">
<twig:Drawer:Trigger>
<twig:Button variant="outline" class="capitalize" {{ ...drawer_trigger_attrs }}>{{ direction }}</twig:Button>
</twig:Drawer:Trigger>
<twig:Drawer:Content class="data-[direction=bottom]:max-h-[50vh] data-[direction=top]:max-h-[50vh]">
<twig:Drawer:Header>
<twig:Drawer:Title>Move Goal</twig:Drawer:Title>
<twig:Drawer:Description>Set your daily activity goal.</twig:Drawer:Description>
</twig:Drawer:Header>
<div class="min-h-0 flex-1 overflow-y-auto px-4">
{% for i in 1..8 %}
<p class="mb-2 leading-relaxed">Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
{% endfor %}
</div>
<twig:Drawer:Footer>
<twig:Button>Submit</twig:Button>
<twig:Drawer:Close>
<twig:Button variant="outline" {{ ...drawer_close_attrs }}>Cancel</twig:Button>
</twig:Drawer:Close>
</twig:Drawer:Footer>
</twig:Drawer:Content>
</twig:Drawer>
{% endfor %}
</div>
RTL
To enable RTL support, set the dir="rtl" attribute on the root element.
100%
Loading...
<div style="min-height: 400px">
<twig:Drawer id="drawer-rtl" dir="rtl">
<twig:Drawer:Trigger>
<twig:Button variant="outline" {{ ...drawer_trigger_attrs }}>فتح</twig:Button>
</twig:Drawer:Trigger>
<twig:Drawer:Content>
<div class="mx-auto w-full max-w-sm">
<twig:Drawer:Header>
<twig:Drawer:Title>تعديل الهدف</twig:Drawer:Title>
<twig:Drawer:Description>حدد هدف نشاطك اليومي.</twig:Drawer:Description>
</twig:Drawer:Header>
<twig:Drawer:Footer>
<twig:Button>إرسال</twig:Button>
<twig:Drawer:Close>
<twig:Button variant="outline" {{ ...drawer_close_attrs }}>إلغاء</twig:Button>
</twig:Drawer:Close>
</twig:Drawer:Footer>
</div>
</twig:Drawer:Content>
</twig:Drawer>
</div>
Accessibility
Drawer:Contentrenders a native<dialog>opened withshowModal(), so the browser traps focus, makes the rest of the page inert and exposes the modal semantics.- The
<dialog>is labelled byDrawer:Titleand described byDrawer:Descriptionthrougharia-labelledbyandaria-describedbybuilt from theidprop. Render both, since the references point at those IDs whether or not the elements exist. Escapecloses the drawer, and so does a click on the backdrop.Drawer:Triggerexposesaria-haspopup="dialog"and anaria-expandedthat the controller keeps in sync.- The drag handle shown on a bottom drawer is decorative. Keep a real close control in the drawer, since a handle cannot be operated from the keyboard.
API Reference
<twig:Drawer>
| Prop | Type | Default |
|---|---|---|
id Unique identifier used to generate internal Drawer IDs.
|
string |
- |
direction Which edge the drawer slides in from.
|
'top'|'right'|'bottom'|'left' |
'bottom' |
open Whether the drawer is open on initial render.
|
boolean |
false |
| Block | Description |
|---|---|
content |
The drawer structure, typically includes Drawer:Trigger and Drawer:Content. |
<twig:Drawer:Close>
| Block | Description |
|---|---|
content |
The close trigger element (e.g., a Button) that closes the drawer when clicked. |
<twig:Drawer:Content>
| Block | Description |
|---|---|
content |
The drawer content, typically includes Drawer:Header and optionally Drawer:Footer. |
<twig:Drawer:Description>
| Block | Description |
|---|---|
content |
The descriptive text explaining the drawer purpose. |
<twig:Drawer:Footer>
| Block | Description |
|---|---|
content |
The footer area, typically contains action buttons. |
<twig:Drawer:Header>
| Block | Description |
|---|---|
content |
The header area, typically contains Drawer:Title and Drawer:Description. |
<twig:Drawer:Title>
| Block | Description |
|---|---|
content |
The title text of the drawer. |
<twig:Drawer:Trigger>
| Block | Description |
|---|---|
content |
The trigger element (e.g., a Button) that opens the drawer when clicked. |