Dialog

A window overlaid on either the primary window or another dialog window, rendering the content underneath inert.

Loading...
<twig:Dialog id="delete_account">
    <twig:Dialog:Trigger>
        <twig:Button {{ ...trigger_attrs }}>Open</twig:Button>
    </twig:Dialog:Trigger>
    <twig:Dialog:Content>
        <twig:Dialog:Header>
            <twig:Dialog:Title>Are you absolutely sure?</twig:Dialog:Title>
            <twig:Dialog:Description>
                This action cannot be undone. This will permanently delete your account
                and remove your data from our servers.
            </twig:Dialog:Description>
        </twig:Dialog:Header>
    </twig:Dialog:Content>
</twig:Dialog>

Installation

Ensure the Symfony UX Toolkit is installed in your Symfony app:

$ composer require --dev symfony/ux-toolkit

Then, run the following command to install the component and its dependencies:

$ bin/console ux:install dialog --kit shadcn

The UX Toolkit is not mandatory to install a component. You can install it manually by following the next steps:

  1. Copy the following file(s) into your Symfony app:
    assets/controllers/dialog_controller.js
    import { Controller } from '@hotwired/stimulus';
    
    export default class extends Controller {
    
        static targets = ['trigger', 'dialog'];
    
        open() {
            this.dialogTarget.showModal();
    
            if (this.hasTriggerTarget) {
                if (this.dialogTarget.getAnimations().length > 0) {
                    this.dialogTarget.addEventListener('transitionend', () => {
                        this.triggerTarget.setAttribute('aria-expanded', 'true');
                    })
                } else {
                    this.triggerTarget.setAttribute('aria-expanded', 'true');
                }
            }
        }
    
        closeOnClickOutside({ target }) {
            if (target === this.dialogTarget) {
                this.close()
            }
        }
    
        close() {
            this.dialogTarget.close();
    
            if (this.hasTriggerTarget) {
                if (this.dialogTarget.getAnimations().length > 0) {
                    this.dialogTarget.addEventListener('transitionend', () => {
                        this.triggerTarget.setAttribute('aria-expanded', 'false');
                    })
                } else {
                    this.triggerTarget.setAttribute('aria-expanded', 'false');
                }
            }
        }
    }
    
    templates/components/Dialog.html.twig
    {# @prop open boolean Open (or not) the Dialog at initial rendering, default to `false` #}
    {# @prop id string Unique suffix identifier for generating Dialog internal IDs #}
    {# @block content The default block #}
    {%- props open = false, id -%}
    
    {%- set _dialog_id = 'dialog-' ~ id -%}
    {%- set _dialog_title_id = _dialog_id ~ '-title' -%}
    {%- set _dialog_description_id = _dialog_id ~ '-description' -%}
    <div {{ attributes.defaults({
        'data-controller': 'dialog',
        'aria-labelledby': _dialog_title_id,
        'aria-describedby': _dialog_description_id,
    }) }}>
        {% block content %}{% endblock %}
    </div>
    
    templates/components/Dialog/Close.html.twig
    {# @block content The default block #}
    {%- set close_attrs = {
        'data-action': 'click->dialog#close',
    } -%}
    {%- block content %}{% endblock -%}
    
    templates/components/Dialog/Content.html.twig
    {# @prop showCloseButton boolean Whether the close button is shown, default to `true` #}
    {# @block content The default block #}
    {%- props showCloseButton = true -%}
    
    <dialog
        id="{{ _dialog_id }}"
        class="{{ 'bg-background fixed top-[50%] left-[50%] z-50 max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] scale-95 gap-4 rounded-lg border p-6 opacity-0 shadow-lg transition-all transition-discrete duration-200 backdrop:transition-discrete backdrop:duration-150 open:grid open:scale-100 open:opacity-100 open:backdrop:bg-black/50 sm:max-w-lg starting:open:scale-95 starting:open:opacity-0 ' ~ attributes.render('class')|tailwind_merge }}"
        data-dialog-target="dialog"
        data-action="keydown.esc->dialog#close:prevent click->dialog#closeOnClickOutside"
    >
        {%- block content %}{% endblock -%}
        {% if showCloseButton %}
            <button type="button" class="ring-offset-background focus:ring-ring absolute top-4 right-4 rounded-xs opacity-70 transition-opacity hover:opacity-100 focus:ring-2 focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4" data-action="click->dialog#close">
                <twig:ux:icon name="lucide:x" />
                <span class="sr-only">Close</span>
            </button>
        {% endif %}
    </dialog>
    
    templates/components/Dialog/Description.html.twig
    {# @block content The default block #}
    <p
        id="{{ _dialog_description_id }}"
        class="{{ 'text-muted-foreground text-sm ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes.without('id') }}
    >
        {%- block content %}{% endblock -%}
    </p>
    
    templates/components/Dialog/Footer.html.twig
    {# @block content The default block #}
    <footer
        class="{{ 'flex flex-col-reverse gap-2 sm:flex-row sm:justify-end ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </footer>
    
    templates/components/Dialog/Header.html.twig
    {# @block content The default block #}
    <header
        class="{{ 'flex flex-col gap-2 text-center sm:text-left ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes }}
    >
        {%- block content %}{% endblock -%}
    </header>
    
    templates/components/Dialog/Title.html.twig
    {# @block content The default block #}
    <h2
        id="{{ _dialog_title_id }}"
        class="{{ 'text-lg leading-none font-semibold ' ~ attributes.render('class')|tailwind_merge }}"
        {{ attributes.without('id') }}
    >
        {%- block content %}{% endblock -%}
    </h2>
    
    templates/components/Dialog/Trigger.html.twig
    {# @block content The default block #}
    {%- set trigger_attrs = {
        'data-action': 'click->dialog#open',
        'data-dialog-target': 'trigger',
        'aria-haspopup': 'dialog',
    } -%}
    {%- block content %}{% endblock -%}
    
  2. If necessary, install the following Composer dependencies:
    $ composer require symfony/ux-icons twig/extra-bundle twig/html-extra:^3.12.0 tales-from-a-dev/twig-tailwind-extra:^1.0.0
  3. And the most important, enjoy!

Usage

<twig:Dialog id="delete_account">
    <twig:Dialog:Trigger>
        <twig:Button {{ ...trigger_attrs }}>Open</twig:Button>
    </twig:Dialog:Trigger>
    <twig:Dialog:Content>
        <twig:Dialog:Header>
            <twig:Dialog:Title>Are you absolutely sure?</twig:Dialog:Title>
            <twig:Dialog:Description>
                This action cannot be undone. This will permanently delete your account
                and remove your data from our servers.
            </twig:Dialog:Description>
        </twig:Dialog:Header>
    </twig:Dialog:Content>
</twig:Dialog>

Examples

Default

Loading...
<twig:Dialog id="delete_account">
    <twig:Dialog:Trigger>
        <twig:Button {{ ...trigger_attrs }}>Open</twig:Button>
    </twig:Dialog:Trigger>
    <twig:Dialog:Content>
        <twig:Dialog:Header>
            <twig:Dialog:Title>Are you absolutely sure?</twig:Dialog:Title>
            <twig:Dialog:Description>
                This action cannot be undone. This will permanently delete your account
                and remove your data from our servers.
            </twig:Dialog:Description>
        </twig:Dialog:Header>
    </twig:Dialog:Content>
</twig:Dialog>

Custom close button

Loading...
<twig:Dialog id="share_link">
    <twig:Dialog:Trigger>
        <twig:Button variant="outline" {{ ...trigger_attrs }}>Share</twig:Button>
    </twig:Dialog:Trigger>
    <twig:Dialog:Content class="sm:max-w-md">
        <twig:Dialog:Header>
            <twig:Dialog:Title>Share link</twig:Dialog:Title>
            <twig:Dialog:Description>
                Anyone who has this link will be able to view this.
            </twig:Dialog:Description>
        </twig:Dialog:Header>
        <div class="flex items-center gap-2">
            <div class="grid flex-1 gap-2">
                <twig:Label for="link" class="sr-only">Link</twig:Label>
                <twig:Input id="link" value="https://ui.shadcn.com/docs/installation" readonly />
          </div>
        </div>
        <twig:Dialog:Footer class="sm:justify-start">
            <twig:Dialog:Close>
                <twig:Button type="button" variant="secondary" {{ ...close_attrs }}>
                    Close
                </twig:Button>
            </twig:Dialog:Close>
        </twig:Dialog:Footer>
    </twig:Dialog:Content>
</twig:Dialog>

With form

Loading...
<twig:Dialog id="edit_profile">
    <twig:Dialog:Trigger>
        <twig:Button {{ ...trigger_attrs }} variant="outline">Open Dialog</twig:Button>
    </twig:Dialog:Trigger>
    <twig:Dialog:Content class="sm:max-w-[425px]">
        <twig:Dialog:Header>
            <twig:Dialog:Title>Edit profile</twig:Dialog:Title>
            <twig:Dialog:Description>
                Make changes to your profile here. Click save when you&apos;re done.
            </twig:Dialog:Description>
        </twig:Dialog:Header>
        <div class="grid gap-4">
            <div class="grid gap-3">
              <twig:Label for="name">Name</twig:Label>
              <twig:Input id="name" name="name" value="Pedro Duarte" />
            </div>
            <div class="grid gap-3">
              <twig:Label for="username">Username</twig:Label>
              <twig:Input id="username" name="username" value="@peduarte" />
          </div>
      </div>
        <twig:Dialog:Footer>
            <twig:Dialog:Close>
                <twig:Button variant="outline" {{ ...close_attrs }}>Cancel</twig:Button>
            </twig:Dialog:Close>
            <twig:Button type="submit">Save changes</twig:Button>
        </twig:Dialog:Footer>
    </twig:Dialog:Content>
</twig:Dialog>

API Reference

Dialog

Prop Type Description
open boolean Open (or not) the Dialog at initial rendering, default to false
id string Unique suffix identifier for generating Dialog internal IDs
Block Description
content The default block

Dialog:Close

Block Description
content The default block

Dialog:Content

Prop Type Description
showCloseButton boolean Whether the close button is shown, default to true
Block Description
content The default block

Dialog:Description

Block Description
content The default block

Dialog:Footer

Block Description
content The default block

Dialog:Header

Block Description
content The default block

Dialog:Title

Block Description
content The default block

Dialog:Trigger

Block Description
content The default block