Modal

Use the modal component to show interactive dialogs and notifications to your website users available in multiple sizes, colors, and styles

Loading...
<twig:Modal id="delete_account">
    <twig:Modal:Trigger>
        <twig:Button variant="outline" {{ ...modal_trigger_attrs }}>Open Modal</twig:Button>
    </twig:Modal:Trigger>

    <twig:Modal:Content>
        <twig:Modal:Header>
            <twig:Modal:Title>Edit profile</twig:Modal:Title>
        </twig:Modal:Header>

        <twig:Modal:Body>
            <p class="leading-relaxed text-body">
                With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, companies around the world are updating their terms of service agreements to comply.
            </p>
            <p class="leading-relaxed text-body">
                The European Union’s General Data Protection Regulation (G.D.P.R.) goes into effect on May 25 and is meant to ensure a common set of data rights in the European Union. It requires organizations to notify users as soon as possible of high-risk data breaches that could personally affect them.
            </p>
        </twig:Modal:Body>

        <twig:Modal:Footer>
            <twig:Button type="submit">I accept</twig:Button>
            <twig:Modal:Close>
                <twig:Button variant="outline" {{ ...modal_close_attrs }}>Decline</twig:Button>
            </twig:Modal:Close>
        </twig:Modal:Footer>
    </twig:Modal:Content>
</twig:Modal>

Installation

bin/console ux:install modal --kit flowbite-4

That's it!

Install the following Composer dependencies:

composer require symfony/ux-icons twig/extra-bundle twig/html-extra:^3.24.0 tales-from-a-dev/twig-tailwind-extra:^1.0.0

Copy the following file(s) into your Symfony app:

assets/controllers/modal_controller.js
import { Controller } from '@hotwired/stimulus';

export default class extends Controller {
    static targets = ['trigger', 'modal'];

    static values = {
        open: Boolean,
    };

    connect() {
        if (this.openValue) {
            this.open();
        }
    }

    open() {
        this.modalTarget.showModal();

        if (this.hasTriggerTarget) {
            if (this.modalTarget.getAnimations().length > 0) {
                this.modalTarget.addEventListener(
                    'transitionend',
                    () => {
                        this.triggerTarget.setAttribute('aria-expanded', 'true');
                        this.modalTarget.setAttribute('aria-hidden', 'false');
                    },
                    { once: true }
                );
            } else {
                this.triggerTarget.setAttribute('aria-expanded', 'true');
                this.modalTarget.setAttribute('aria-hidden', 'false');
            }
        }
    }

    closeOnClickOutside({ target }) {
        if (target === this.modalTarget) {
            this.close();
        }
    }

    close() {
        this.modalTarget.close();

        if (this.hasTriggerTarget) {
            if (this.modalTarget.getAnimations().length > 0) {
                this.modalTarget.addEventListener(
                    'transitionend',
                    () => {
                        this.triggerTarget.setAttribute('aria-expanded', 'false');
                        this.modalTarget.setAttribute('aria-hidden', 'true');
                    },
                    { once: true }
                );
            } else {
                this.triggerTarget.setAttribute('aria-expanded', 'false');
                this.modalTarget.setAttribute('aria-hidden', 'true');
            }
        }
    }
}
templates/components/Modal.html.twig
{# @prop open boolean Whether the modal is open on initial render. Defaults to `false` #}
{# @prop id string Unique identifier used to generate internal Modal IDs #}
{# @block content The modal structure, typically includes `Modal:Trigger` and `Modal:Content` #}
{%- props open = false, id -%}

{%- set _modal_id = 'modal-' ~ id -%}
{%- set _modal_title_id = _modal_id ~ '-title' -%}
<div {{ attributes.defaults({
    'data-controller': 'modal',
    'data-modal-open-value': open,
    'aria-labelledby': _modal_title_id,
}) }}>
    {% block content %}{% endblock %}
</div>
templates/components/Modal/Body.html.twig
{# @block content The main content area of the moda #}
<div
    class="{{ ('space-y-4 md:space-y-6 py-4 md:py-6 ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</div>
templates/components/Modal/Close.html.twig
{# @block content The close trigger element (e.g., a `Button`) that closes the modal when clicked #}
{%- set modal_close_attrs = {
    'data-action': 'click->modal#close',
} -%}
{%- block content %}{% endblock -%}
templates/components/Modal/Content.html.twig
{# @prop showCloseButton boolean Whether to display the close button in the top-right corner. Defaults to `true` #}
{# @prop backdrop 'dynamic'|'static' To prevent the modal from closing when clicking outside. Defaults to `dynamic` #}
{# @block content The modal content, typically includes `Modal:Header` and optionally `Modal:Footer` #}
{%- props showCloseButton = true, backdrop = 'dynamic' -%}

<dialog
    id="{{ _modal_id }}"
    tabindex="-1"
    aria-hidden="true"
    class="{{ ('relative bg-neutral-primary-soft text-body border border-default rounded-base shadow-sm p-4 md:p-6 m-auto z-50 backdrop:transition-discrete backdrop:duration-150 open:backdrop:bg-dark-backdrop/70 ' ~ attributes.render('class'))|tailwind_merge }}"
    data-modal-target="modal"
    data-action="keydown.esc->modal#close:prevent {{ backdrop is not same as('static') ? 'click->modal#closeOnClickOutside' }}"
>
    {%- block content %}{% endblock -%}
    {% if showCloseButton %}
        <twig:Button type="button" variant="ghost" size="icon-xs" class="absolute top-4 right-4" data-action="click->modal#close">
            <twig:ux:icon name="flowbite:close-outline" class="size-4" />
            <span class="sr-only">Close</span>
        </twig:Button>
    {% endif %}
</dialog>
templates/components/Modal/Footer.html.twig
{# @block content The footer area, typically contains action buttons #}
<footer
    class="{{ ('flex items-center border-t border-default space-x-4 pt-4 md:pt-5 ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</footer>
templates/components/Modal/Header.html.twig
{# @block content The header area, typically contains `Modal:Title` and `Modal:Description` #}
<header
    class="{{ ('flex items-center justify-between border-b border-default pb-4 md:pb-5 ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes }}
>
    {%- block content %}{% endblock -%}
</header>
templates/components/Modal/Title.html.twig
{# @block content The title text of the modal #}
<h3
    id="{{ _modal_title_id }}"
    class="{{ ('text-lg font-medium text-heading ' ~ attributes.render('class'))|tailwind_merge }}"
    {{ attributes.without('id') }}
>
    {%- block content %}{% endblock -%}
</h3>
templates/components/Modal/Trigger.html.twig
{# @block content The trigger element (e.g., a `Button`) that opens the modal when clicked #}
{%- set modal_trigger_attrs = {
    'data-action': 'click->modal#open'|html_attr_type('sst'),
    'data-modal-target': 'trigger',
    'aria-haspopup': 'dialog',
} -%}
{%- block content %}{% endblock -%}

Happy coding!

Usage

<twig:Modal id="delete_account">
    <twig:Modal:Trigger>
        <twig:Button {{ ...modal_trigger_attrs }}>Open</twig:Button>
    </twig:Modal:Trigger>
    <twig:Modal:Content>
        <twig:Modal:Header>
            <twig:Modal:Title>Are you absolutely sure?</twig:Modal:Title>
        </twig:Modal:Header>
    </twig:Modal:Content>
</twig:Modal>

Examples

Static modal

Use the prop backdrop="static" to prevent the modal from closing when clicking outside of it. This can be used with situations where you want to force the user to choose an option such as a cookie notice or when taking a survey.

Loading...
<twig:Modal id="delete_account">
    <twig:Modal:Trigger>
        <twig:Button variant="outline" {{ ...modal_trigger_attrs }}>Open Modal</twig:Button>
    </twig:Modal:Trigger>

    <twig:Modal:Content class="max-w-[600px]" backdrop="static">
        <twig:Modal:Header>
            <twig:Modal:Title>Edit profile</twig:Modal:Title>
        </twig:Modal:Header>

        <twig:Modal:Body>
            <p class="leading-relaxed text-body">
                Lorem ipsum dolor sit amet, consectetur adipiscing elit. Aenean placerat, velit sit amet interdum auctor, ligula lorem posuere urna, ut lobortis odio odio et leo. Nam scelerisque vel sem vel pulvinar.
            </p>
        </twig:Modal:Body>

        <twig:Modal:Footer>
            <twig:Button type="submit">I accept</twig:Button>
            <twig:Modal:Close>
                <twig:Button variant="outline" {{ ...modal_close_attrs }}>Decline</twig:Button>
            </twig:Modal:Close>
        </twig:Modal:Footer>
    </twig:Modal:Content>
</twig:Modal>

Pop-up modal

You can use this modal example to show a pop-up decision dialog to your users especially when deleting an item and making sure if the user really wants to do that by double confirming.

Loading...
<twig:Modal id="share_link">
    <twig:Modal:Trigger>
        <twig:Button variant="outline-danger" {{ ...modal_trigger_attrs }}>Delete</twig:Button>
    </twig:Modal:Trigger>

    <twig:Modal:Content class="sm:max-w-md" :showCloseButton="false">

        <twig:Modal:Body class="text-center">
            <twig:ux:icon name="flowbite:exclamation-circle-outline" class="mx-auto size-12 text-fg-disabled" aria-hidden="true" />
            <twig:Modal:Title>Are you sure you want to delete this product from your account?</twig:Modal:Title>
            <div class="flex items-center space-x-4 justify-center">
                <twig:Button variant="danger">
                    Yes, I'm sure
                </twig:Button>

                <twig:Modal:Close>
                    <twig:Button variant="outline" {{ ...modal_close_attrs }}>
                        No, cancel
                    </twig:Button>
                </twig:Modal:Close>
            </div>
        </twig:Modal:Body>

    </twig:Modal:Content>
</twig:Modal>

Opened by default

Loading...
<twig:Modal id="delete_account" open>
    <twig:Modal:Trigger>
        <twig:Button variant="outline" {{ ...modal_trigger_attrs }}>Open Modal</twig:Button>
    </twig:Modal:Trigger>

    <twig:Modal:Content>
        <twig:Modal:Header>
            <twig:Modal:Title>Edit profile</twig:Modal:Title>
        </twig:Modal:Header>

        <twig:Modal:Body>
            <p class="leading-relaxed text-body">
                With less than a month to go before the European Union enacts new consumer privacy laws for its citizens, companies around the world are updating their terms of service agreements to comply.
            </p>
            <p class="leading-relaxed text-body">
                The European Union’s General Data Protection Regulation (G.D.P.R.) goes into effect on May 25 and is meant to ensure a common set of data rights in the European Union. It requires organizations to notify users as soon as possible of high-risk data breaches that could personally affect them.
            </p>
        </twig:Modal:Body>

        <twig:Modal:Footer>
            <twig:Button type="submit">I accept</twig:Button>
            <twig:Modal:Close>
                <twig:Button variant="outline" {{ ...modal_close_attrs }}>Decline</twig:Button>
            </twig:Modal:Close>
        </twig:Modal:Footer>
    </twig:Modal:Content>
</twig:Modal>

API Reference

Modal

Prop Type Description
open boolean Whether the modal is open on initial render. Defaults to false
id string Unique identifier used to generate internal Modal IDs
Block Description
content The modal structure, typically includes Modal:Trigger and Modal:Content

Modal:Body

Block Description
content The main content area of the moda

Modal:Close

Block Description
content The close trigger element (e.g., a Button) that closes the modal when clicked

Modal:Content

Prop Type Description
showCloseButton boolean Whether to display the close button in the top-right corner. Defaults to true
backdrop 'dynamic'|'static' To prevent the modal from closing when clicking outside. Defaults to dynamic
Block Description
content The modal content, typically includes Modal:Header and optionally Modal:Footer

Modal:Footer

Block Description
content The footer area, typically contains action buttons

Modal:Header

Block Description
content The header area, typically contains Modal:Title and Modal:Description

Modal:Title

Block Description
content The title text of the modal

Modal:Trigger

Block Description
content The trigger element (e.g., a Button) that opens the modal when clicked