Popover
Display contextual Bootstrap content beside a trigger element.
<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 'top'|'right'|'bottom'|'left'|'auto' 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 = '',
placement = 'right',
trigger = 'click',
html = false,
container = null,
customClass = null
-%}
{%- set popover_trigger_attrs = {
'data-bs-toggle': 'popover',
}|html_attr_merge -%}
{%- if content is not empty -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-content': content}) -%}
{%- endif -%}
{%- if placement != 'right' -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-placement': placement}) -%}
{%- endif -%}
{%- if trigger != 'click' -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-trigger': trigger}) -%}
{%- endif -%}
{%- if title is not null and title is not empty -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-title': title}) -%}
{%- endif -%}
{%- if html -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-html': 'true'}) -%}
{%- endif -%}
{%- if container is not null and container is not empty -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-container': container}) -%}
{%- endif -%}
{%- if customClass is not null and customClass is not empty -%}
{%- set popover_trigger_attrs = popover_trigger_attrs|merge({'data-bs-custom-class': 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.
<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.
<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.
<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.
<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.
<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.
<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.
<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 The optional heading displayed in the popover.
|
string|null |
null |
content The popover body content.
|
string |
'' |
placement The preferred placement relative to the trigger.
|
'top'|'right'|'bottom'|'left'|'auto' |
'right' |
trigger The Bootstrap interaction or space-separated interactions that open and close the popover.
|
string |
'click' |
html Whether the popover body may contain sanitized HTML.
|
boolean |
false |
container The optional selector of the element that receives the generated popover.
|
string|null |
null |
customClass The optional class added to the generated popover.
|
string|null |
null |
| Block | Description |
|---|---|
content |
The trigger element, which must render popover_trigger_attrs with html_attr(). |