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 |