View as Markdown

Modal

Add accessible dialog overlays for notifications, forms, and custom content.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#demo-modal">
    Review changes
</button>

<twig:Modal id="demo-modal" centered>
    <twig:Modal:Header>
        <twig:Modal:Title>Review changes</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <p>Your profile information is ready to be saved.</p>
        <p class="mb-0 text-body-secondary">You can return to editing or confirm these changes now.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Keep editing</button>
        <button type="button" class="btn btn-primary" data-bs-dismiss="modal">Save changes</button>
    </twig:Modal:Footer>
</twig:Modal>

Installation

php bin/console ux:install modal --kit bootstrap

Install the following Composer dependencies:

composer require symfony/ux-twig-component:^3.1 twig/extra-bundle twig/html-extra:^3.12.0

Install the following front-end dependencies:

npm install bootstrap@^5.3.0
# ...or with Symfony AssetMapper
php bin/console importmap:require bootstrap bootstrap/dist/css/bootstrap.min.css

Copy the following file(s) into your app:

{# @prop id string The unique identifier targeted by modal controls. #}
{# @prop size &#039;sm&#039;|&#039;lg&#039;|&#039;xl&#039;|null The optional maximum width of the modal dialog. #}
{# @prop centered boolean Whether to vertically center the modal dialog. #}
{# @prop scrollable boolean Whether only the modal body scrolls when content overflows. #}
{# @prop fullscreen boolean|&#039;sm&#039;|&#039;md&#039;|&#039;lg&#039;|&#039;xl&#039;|&#039;xxl&#039; Whether and below which breakpoint the modal fills the viewport. #}
{# @prop backdrop boolean|&#039;static&#039; Whether clicking the backdrop dismisses the modal. #}
{# @prop keyboard boolean Whether pressing Escape dismisses the modal. #}
{# @prop fade boolean Whether to use Bootstrap&#039;s fade transition. #}
{# @block content The modal structure, typically `Modal:Header`, `Modal:Body`, and `Modal:Footer`. #}
{%- props
    id,
    size = null,
    centered = false,
    scrollable = false,
    fullscreen = false,
    backdrop = true,
    keyboard = true,
    fade = true
-%}
{%- set title_id = id ~ &#039;-label&#039; -%}
{%- do provide(&#039;modal.titleId&#039;, title_id) -%}
{%- set fullscreen_class = fullscreen is same as(true)
    ? &#039;modal-fullscreen&#039;
    : (fullscreen in [&#039;sm&#039;, &#039;md&#039;, &#039;lg&#039;, &#039;xl&#039;, &#039;xxl&#039;] ? &#039;modal-fullscreen-&#039; ~ fullscreen ~ &#039;-down&#039; : null)
-%}
{%- set modal_classes = html_classes({
    modal: true,
    fade: fade,
}) -%}
{%- set dialog_classes = html_classes({
    &#039;modal-dialog&#039;: true,
    (&#039;modal-&#039; ~ size): size in [&#039;sm&#039;, &#039;lg&#039;, &#039;xl&#039;],
    &#039;modal-dialog-centered&#039;: centered,
    &#039;modal-dialog-scrollable&#039;: scrollable,
}) -%}
{%- if fullscreen_class is not null -%}
    {%- set dialog_classes = dialog_classes ~ &#039; &#039; ~ fullscreen_class -%}
{%- endif -%}
{%- set default_attrs = {
    id: id,
    class: modal_classes,
    tabindex: &#039;-1&#039;,
    &#039;aria-labelledby&#039;: title_id,
    &#039;aria-hidden&#039;: &#039;true&#039;,
} -%}
{%- if backdrop is not same as(true) -%}
    {%- set default_attrs = default_attrs|merge({&#039;data-bs-backdrop&#039;: backdrop is same as(false) ? &#039;false&#039; : &#039;static&#039;}) -%}
{%- endif -%}
{%- if not keyboard -%}
    {%- set default_attrs = default_attrs|merge({&#039;data-bs-keyboard&#039;: &#039;false&#039;}) -%}
{%- endif -%}
&lt;div {{ attributes.defaults(default_attrs) }}&gt;
    &lt;div class=&quot;{{ dialog_classes }}&quot;&gt;
        &lt;div class=&quot;modal-content&quot;&gt;
            {%- block content %}{% endblock -%}
        &lt;/div&gt;
    &lt;/div&gt;
&lt;/div&gt;
{# @block content The main content of the modal. #}
&lt;div {{ attributes.defaults({class: &#039;modal-body&#039;}) }}&gt;
    {%- block content %}{% endblock -%}
&lt;/div&gt;
{# @block content The modal actions and supplementary content. #}
&lt;div {{ attributes.defaults({class: &#039;modal-footer&#039;}) }}&gt;
    {%- block content %}{% endblock -%}
&lt;/div&gt;
{# @prop closable boolean Whether to render a dismiss button. #}
{# @prop closeLabel string The accessible label of the dismiss button. #}
{# @block content The modal heading, typically a `Modal:Title` component. #}
{%- props closable = true, closeLabel = &#039;Close&#039;|trans -%}
&lt;div {{ attributes.defaults({class: &#039;modal-header&#039;}) }}&gt;
    {%- block content %}{% endblock -%}
    {%- if closable %}
        &lt;button type=&quot;button&quot; class=&quot;btn-close&quot; data-bs-dismiss=&quot;modal&quot; aria-label=&quot;{{ closeLabel }}&quot;&gt;&lt;/button&gt;
    {%- endif %}
&lt;/div&gt;
{# @prop tag &#039;h1&#039;|&#039;h2&#039;|&#039;h3&#039;|&#039;h4&#039;|&#039;h5&#039;|&#039;h6&#039; The heading element to render. #}
{# @block content The modal title. #}
{%- props tag = &#039;h1&#039; -%}
{%- set _modal_title_id = inject(&#039;modal.titleId&#039;, null) -%}
{%- set final_tag = tag in [&#039;h1&#039;, &#039;h2&#039;, &#039;h3&#039;, &#039;h4&#039;, &#039;h5&#039;, &#039;h6&#039;] ? tag : &#039;h1&#039; -%}
{%- set default_attrs = _modal_title_id is null
    ? {class: &#039;modal-title fs-5&#039;}
    : {id: _modal_title_id, class: &#039;modal-title fs-5&#039;}
-%}
&lt;{{ final_tag }} {{ attributes.defaults(default_attrs) }}&gt;
    {%- block content %}{% endblock -%}
&lt;/{{ final_tag }}&gt;

Usage

<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#usage-modal">
    Open modal
</button>

<twig:Modal id="usage-modal">
    <twig:Modal:Header>
        <twig:Modal:Title>Modal title</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>Modal body content goes here.</twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
    </twig:Modal:Footer>
</twig:Modal>

Accessibility

Include a Modal:Title so the root modal's aria-labelledby points to a real heading. A modal represents a separate document context, so an h1 is usually appropriate; use the tag prop when the surrounding hierarchy requires another level.

Bootstrap adds the dialog role and manages focus while the modal is open. The HTML autofocus attribute does not focus controls inside a modal; listen for shown.bs.modal when an initial focus target is required. Do not nest modals, and always provide an explicit dismiss action.

Examples

Modal components

Display the complete modal structure statically when composing or reviewing its content.

100%
Loading...
<twig:Modal id="static-modal" :fade="false" class="position-static d-block" aria-hidden="false">
    <twig:Modal:Header>
        <twig:Modal:Title tag="h5">Modal title</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <p>Modal body text goes here.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
    </twig:Modal:Footer>
</twig:Modal>

Live demo

Open and dismiss a standard animated modal through Bootstrap data attributes.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#live-demo-modal">
    Launch demo modal
</button>

<twig:Modal id="live-demo-modal">
    <twig:Modal:Header>
        <twig:Modal:Title>Modal title</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <p>Woo-hoo, you're reading this text in a modal!</p>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
    </twig:Modal:Footer>
</twig:Modal>

Static backdrop

Prevent backdrop clicks and Escape from dismissing a modal that requires an explicit choice.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#static-backdrop-modal">
    Launch static backdrop modal
</button>

<twig:Modal id="static-backdrop-modal" backdrop="static" :keyboard="false">
    <twig:Modal:Header>
        <twig:Modal:Title>Modal title</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <p>I will not close if you click outside of me or press Escape.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Understood</button>
    </twig:Modal:Footer>
</twig:Modal>

Scrolling long content

Let the viewport scroll with a long modal or constrain scrolling to the modal body.

100%
Loading...
<div class="d-flex flex-wrap gap-2">
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#long-content-modal">Launch long modal</button>
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#scrollable-modal">Launch scrollable modal</button>
</div>

<twig:Modal id="long-content-modal">
    <twig:Modal:Header><twig:Modal:Title>Long modal</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>
        <p>This modal grows with its content, allowing the viewport to scroll.</p>
        <div style="min-height: 900px;"></div>
        <p class="mb-0">This content appears at the bottom.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

<twig:Modal id="scrollable-modal" scrollable>
    <twig:Modal:Header><twig:Modal:Title>Scrollable modal</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>
        <p>Only this modal body scrolls when its content exceeds the available height.</p>
        <div style="min-height: 900px;"></div>
        <p class="mb-0">This content appears at the bottom.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

Vertically centered

Center the dialog vertically, with or without an independently scrollable body.

100%
Loading...
<div class="d-flex flex-wrap gap-2">
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#centered-modal">Vertically centered modal</button>
    <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#centered-scrollable-modal">Centered scrollable modal</button>
</div>

<twig:Modal id="centered-modal" centered>
    <twig:Modal:Header><twig:Modal:Title>Modal title</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>This is a vertically centered modal.</twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

<twig:Modal id="centered-scrollable-modal" centered scrollable>
    <twig:Modal:Header><twig:Modal:Title>Modal title</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>
        <p>This modal combines vertical centering with an independently scrollable body.</p>
        <div style="min-height: 900px;"></div>
        <p class="mb-0">Just like that.</p>
    </twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

Tooltips and popovers

Place supplementary Bootstrap overlays inside a modal. Bootstrap dismisses them automatically when the modal closes.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#modal-with-overlays">
    Launch demo modal
</button>

<twig:Modal id="modal-with-overlays">
    <twig:Modal:Header>
        <twig:Modal:Title>Modal title</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <h2 class="fs-5">Popover in a modal</h2>
        <p>
            This
            <twig:Popover title="Popover title" content="Popover body content is set in this attribute.">
                <button class="btn btn-secondary" type="button" {{ html_attr(popover_trigger_attrs) }}>button</button>
            </twig:Popover>
            triggers a popover on click.
        </p>
        <hr>
        <h2 class="fs-5">Tooltips in a modal</h2>
        <p>
            <twig:Tooltip title="Tooltip"><a href="#" {{ html_attr(tooltip_trigger_attrs) }}>This link</a></twig:Tooltip>
            and
            <twig:Tooltip title="Tooltip"><a href="#" {{ html_attr(tooltip_trigger_attrs) }}>that link</a></twig:Tooltip>
            have tooltips on hover.
        </p>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Save changes</button>
    </twig:Modal:Footer>
</twig:Modal>

Using the grid

Nest Bootstrap's responsive grid inside the modal body.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#grid-modal">
    Launch grid modal
</button>

<twig:Modal id="grid-modal" size="lg">
    <twig:Modal:Header><twig:Modal:Title>Grids in modals</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>
        <div class="container-fluid text-center">
            <div class="row mb-3">
                <div class="col-md-4 border py-2">.col-md-4</div>
                <div class="col-md-4 ms-auto border py-2">.col-md-4 .ms-auto</div>
            </div>
            <div class="row mb-3">
                <div class="col-md-3 ms-auto border py-2">.col-md-3 .ms-auto</div>
                <div class="col-md-2 ms-auto border py-2">.col-md-2 .ms-auto</div>
            </div>
            <div class="row mb-3">
                <div class="col-md-6 ms-auto border py-2">.col-md-6 .ms-auto</div>
            </div>
            <div class="row">
                <div class="col-sm-9 border py-2">
                    Level 1: .col-sm-9
                    <div class="row mt-2">
                        <div class="col-8 col-sm-6 border py-2">Level 2: .col-8 .col-sm-6</div>
                        <div class="col-4 col-sm-6 border py-2">Level 2: .col-4 .col-sm-6</div>
                    </div>
                </div>
            </div>
        </div>
    </twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

Varying modal content

Read data attributes from the triggering button to update a shared modal before it is displayed.

100%
Loading...
<div class="d-flex flex-wrap gap-2">
    {% for recipient in ['@mdo', '@fat', '@getbootstrap'] %}
        <button
            type="button"
            class="btn btn-primary"
            data-bs-toggle="modal"
            data-bs-target="#varying-content-modal"
            data-bs-recipient="{{ recipient }}"
        >Open modal for {{ recipient }}</button>
    {% endfor %}
</div>

<twig:Modal id="varying-content-modal">
    <twig:Modal:Header>
        <twig:Modal:Title>New message</twig:Modal:Title>
    </twig:Modal:Header>
    <twig:Modal:Body>
        <form>
            <div class="mb-3">
                <label for="recipient-name" class="col-form-label">Recipient:</label>
                <input type="text" class="form-control" id="recipient-name">
            </div>
            <div class="mb-3">
                <label for="message-text" class="col-form-label">Message:</label>
                <textarea class="form-control" id="message-text"></textarea>
            </div>
        </form>
    </twig:Modal:Body>
    <twig:Modal:Footer>
        <button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button>
        <button type="button" class="btn btn-primary">Send message</button>
    </twig:Modal:Footer>
</twig:Modal>

<script type="module">
    const modal = document.querySelector('#varying-content-modal');

    modal.addEventListener('show.bs.modal', (event) => {
        const recipient = event.relatedTarget.getAttribute('data-bs-recipient');
        modal.querySelector('.modal-title').textContent = `New message to ${recipient}`;
        modal.querySelector('#recipient-name').value = recipient;
    });
</script>

Toggle between modals

Switch between two modals without displaying them at the same time.

100%
Loading...
<button class="btn btn-primary" data-bs-target="#first-toggle-modal" data-bs-toggle="modal">Open first modal</button>

<twig:Modal id="first-toggle-modal" centered>
    <twig:Modal:Header><twig:Modal:Title>Modal 1</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>Show a second modal and hide this one with the button below.</twig:Modal:Body>
    <twig:Modal:Footer>
        <button class="btn btn-primary" data-bs-target="#second-toggle-modal" data-bs-toggle="modal">Open second modal</button>
    </twig:Modal:Footer>
</twig:Modal>

<twig:Modal id="second-toggle-modal" centered>
    <twig:Modal:Header><twig:Modal:Title>Modal 2</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>Hide this modal and show the first with the button below.</twig:Modal:Body>
    <twig:Modal:Footer>
        <button class="btn btn-primary" data-bs-target="#first-toggle-modal" data-bs-toggle="modal">Back to first</button>
    </twig:Modal:Footer>
</twig:Modal>

Remove animation

Disable the fade transition when the modal should appear immediately.

100%
Loading...
<button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#no-animation-modal">
    Open without animation
</button>

<twig:Modal id="no-animation-modal" :fade="false">
    <twig:Modal:Header><twig:Modal:Title>Modal without animation</twig:Modal:Title></twig:Modal:Header>
    <twig:Modal:Body>This modal appears immediately because the <code>.fade</code> class is omitted.</twig:Modal:Body>
    <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
</twig:Modal>

Optional sizes

Choose Bootstrap's small, large, or extra-large modal widths.

100%
Loading...
<div class="d-flex flex-wrap gap-2">
    {% for size, label in {xl: 'Extra large', lg: 'Large', sm: 'Small'} %}
        <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#size-{{ size }}-modal">{{ label }} modal</button>
    {% endfor %}
</div>

{% for size, label in {xl: 'Extra large', lg: 'Large', sm: 'Small'} %}
    <twig:Modal id="size-{{ size }}-modal" size="{{ size }}">
        <twig:Modal:Header><twig:Modal:Title>{{ label }} modal</twig:Modal:Title></twig:Modal:Header>
        <twig:Modal:Body>Modal content at the <code>{{ size }}</code> size.</twig:Modal:Body>
        <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
    </twig:Modal>
{% endfor %}

Fullscreen modal

Fill the viewport at every size or only below a selected breakpoint.

100%
Loading...
{% set variants = {
    always: ['Full screen', true],
    sm: ['Full screen below sm', 'sm'],
    md: ['Full screen below md', 'md'],
    lg: ['Full screen below lg', 'lg'],
    xl: ['Full screen below xl', 'xl'],
    xxl: ['Full screen below xxl', 'xxl'],
} %}

<div class="d-flex flex-wrap gap-2">
    {% for key, variant in variants %}
        <button type="button" class="btn btn-primary" data-bs-toggle="modal" data-bs-target="#fullscreen-{{ key }}-modal">{{ variant[0] }}</button>
    {% endfor %}
</div>

{% for key, variant in variants %}
    <twig:Modal id="fullscreen-{{ key }}-modal" fullscreen="{{ variant[1] }}">
        <twig:Modal:Header><twig:Modal:Title>{{ variant[0] }}</twig:Modal:Title></twig:Modal:Header>
        <twig:Modal:Body>Responsive fullscreen modal content.</twig:Modal:Body>
        <twig:Modal:Footer><button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Close</button></twig:Modal:Footer>
    </twig:Modal>
{% endfor %}

API Reference

<twig:Modal>

Prop Type Default
id 
string -
size 
'sm'|'lg'|'xl'|null null
centered 
boolean false
scrollable 
boolean false
fullscreen 
boolean|'sm'|'md'|'lg'|'xl'|'xxl' false
backdrop 
boolean|'static' true
keyboard 
boolean true
fade 
boolean true
Block Description
content The modal structure, typically Modal:Header, Modal:Body, and Modal:Footer.

<twig:Modal:Body>

Block Description
content The main content of the modal.

<twig:Modal:Footer>

Block Description
content The modal actions and supplementary content.

<twig:Modal:Header>

Prop Type Default
closable 
boolean true
closeLabel 
string -
Block Description
content The modal heading, typically a Modal:Title component.

<twig:Modal:Title>

Prop Type Default
tag 
'h1'|'h2'|'h3'|'h4'|'h5'|'h6' 'h1'
Block Description
content The modal title.