View as Markdown

Popover

Display contextual Bootstrap content beside a trigger element.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover title="Bootstrap popover" content="This content appears beside the trigger.">
        <button type="button" class="btn btn-primary" {{ html_attr(popover_trigger_attrs) }}>
            Toggle popover
        </button>
    </twig:Popover>
</div>

Installation

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

Install the following Composer dependencies:

composer require twig/extra-bundle twig/html-extra:^3.24.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 title string|null The optional heading displayed in the popover. #}
{# @prop content string The popover body content. #}
{# @prop placement &#039;top&#039;|&#039;right&#039;|&#039;bottom&#039;|&#039;left&#039;|&#039;auto&#039; The preferred placement relative to the trigger. #}
{# @prop trigger string The Bootstrap interaction or space-separated interactions that open and close the popover. #}
{# @prop html boolean Whether the popover body may contain sanitized HTML. #}
{# @prop container string|null The optional selector of the element that receives the generated popover. #}
{# @prop customClass string|null The optional class added to the generated popover. #}
{# @block content The trigger element, which must render `popover_trigger_attrs` with `html_attr()`. #}
{%- props
    title = null,
    content = &#039;&#039;,
    placement = &#039;right&#039;,
    trigger = &#039;click&#039;,
    html = false,
    container = null,
    customClass = null
-%}
{%- set popover_trigger_attrs = {
    &#039;data-bs-toggle&#039;: &#039;popover&#039;,
}|html_attr_merge -%}
{%- if content is not empty -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-content&#039;: content}) -%}
{%- endif -%}
{%- if placement != &#039;right&#039; -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-placement&#039;: placement}) -%}
{%- endif -%}
{%- if trigger != &#039;click&#039; -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-trigger&#039;: trigger}) -%}
{%- endif -%}
{%- if title is not null and title is not empty -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-title&#039;: title}) -%}
{%- endif -%}
{%- if html -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-html&#039;: &#039;true&#039;}) -%}
{%- endif -%}
{%- if container is not null and container is not empty -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-container&#039;: container}) -%}
{%- endif -%}
{%- if customClass is not null and customClass is not empty -%}
    {%- set popover_trigger_attrs = popover_trigger_attrs|merge({&#039;data-bs-custom-class&#039;: customClass}) -%}
{%- endif -%}
{%- block content %}{% endblock -%}

Usage

<twig:Popover title="Popover title" content="Helpful contextual information.">
    <button type="button" class="btn btn-secondary" {{ html_attr(popover_trigger_attrs) }}>
        Show popover
    </button>
</twig:Popover>

Bootstrap popovers are opt-in. Initialize them after Bootstrap is loaded:

import { Popover } from 'bootstrap';

document.querySelectorAll('[data-bs-toggle="popover"]').forEach((element) => {
    Popover.getOrCreateInstance(element);
});

The component follows an as-child pattern: render popover_trigger_attrs with Twig's html_attr() function on the actual interactive element so Bootstrap receives the data attributes without an extra wrapper.

Accessibility

Use a button for popovers opened by click. For a dismiss-on-next-click popover, use the focus trigger on an element that can receive keyboard focus. Never rely on hover alone because keyboard and touch users cannot reliably reach that content.

Keep popovers short and supplementary. Their generated markup is not a modal dialog and does not trap focus. Disabled controls cannot receive focus, so attach the popover to a focusable wrapper and keep the disabled control inside it.

When html is enabled, Bootstrap sanitizes allowed markup by default. Do not disable sanitization for untrusted content.

Examples

Enable popovers

Initialize every data-bs-toggle="popover" trigger before expecting it to open.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover content="Popover components are opt-in and must be initialized in JavaScript.">
        <button type="button" class="btn btn-secondary" {{ html_attr(popover_trigger_attrs) }}>
            Popover trigger
        </button>
    </twig:Popover>
</div>

Live demo

Combine a heading and body content in a conventional click-triggered popover.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover
        title="Popover title"
        content="And here's some amazing content. It's very engaging. Right?"
    >
        <button type="button" class="btn btn-lg btn-danger" {{ html_attr(popover_trigger_attrs) }}>
            Click to toggle popover
        </button>
    </twig:Popover>
</div>

Four directions

Prefer top, right, bottom, or left placement while allowing Popper to adjust when space is constrained.

100%
Loading...
<div class="d-flex flex-wrap justify-content-center gap-2 p-5">
    {% for placement in ['left', 'top', 'bottom', 'right'] %}
        <twig:Popover
            title="Popover {{ placement }}"
            content="On the {{ placement }} side."
            :placement="placement"
        >
            <button type="button" class="btn btn-secondary text-capitalize" {{ html_attr(popover_trigger_attrs) }}>
                {{ placement }}
            </button>
        </twig:Popover>
    {% endfor %}
</div>

Custom container

Append the generated popover to a specific container when the surrounding layout requires it.

100%
Loading...
<div id="popover-container" class="border rounded p-5 text-center w-100">
    <twig:Popover
        title="Contained popover"
        content="Bootstrap appends this popover inside the selected container."
        container="#popover-container"
    >
        <button type="button" class="btn btn-secondary" {{ html_attr(popover_trigger_attrs) }}>
            Use custom container
        </button>
    </twig:Popover>
</div>

Custom popovers

Attach a custom class to the generated popover and customize Bootstrap's CSS variables in your stylesheet.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover
        title="Custom popover"
        content="Use the generated class to customize Bootstrap CSS variables."
        customClass="custom-popover"
    >
        <button type="button" class="btn btn-secondary" {{ html_attr(popover_trigger_attrs) }}>
            Custom popover
        </button>
    </twig:Popover>
</div>

Dismiss on next click

Use the focus trigger so moving focus away closes the popover.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover
        title="Dismissible popover"
        content="Move focus to another element to dismiss this popover."
        trigger="focus"
    >
        <a class="btn btn-lg btn-danger" href="#" role="button" tabindex="0" {{ html_attr(popover_trigger_attrs) }}>
            Dismissible popover
        </a>
    </twig:Popover>
</div>

Disabled elements

Place a disabled control inside a focusable wrapper that owns the popover attributes.

100%
Loading...
<div class="d-flex w-100">
    <twig:Popover
        content="Disabled controls cannot receive focus, so the wrapper owns the popover."
        trigger="hover focus"
    >
        <span class="d-inline-block" tabindex="0" {{ html_attr(popover_trigger_attrs) }}>
            <button class="btn btn-primary" type="button" disabled>Disabled button</button>
        </span>
    </twig:Popover>
</div>

API Reference

<twig:Popover>

Prop Type Default
title 
string|null null
content 
string ''
placement 
'top'|'right'|'bottom'|'left'|'auto' 'right'
trigger 
string 'click'
html 
boolean false
container 
string|null null
customClass 
string|null null
Block Description
content The trigger element, which must render popover_trigger_attrs with html_attr().