Offcanvas
Build responsive sliding panels for navigation, forms, and supplementary content.
<div class="d-flex justify-content-center w-100">
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#demo-offcanvas" aria-controls="demo-offcanvas">
Launch demo offcanvas
</button>
<twig:Offcanvas id="demo-offcanvas">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Content for the offcanvas goes here. You can place just about any Bootstrap component or custom element here.</p>
<div class="dropdown mt-3">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Dropdown button</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
</ul>
</div>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Installation
php bin/console ux:install offcanvas --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 offcanvas controls. #}
{# @prop placement 'start'|'end'|'top'|'bottom' The viewport edge from which the panel appears. #}
{# @prop responsive 'sm'|'md'|'lg'|'xl'|'xxl'|null The breakpoint at which the panel becomes regular content. #}
{# @prop backdrop boolean|'static' Whether clicking the backdrop dismisses the panel. #}
{# @prop scroll boolean Whether the page body remains scrollable while the panel is open. #}
{# @prop keyboard boolean Whether pressing Escape dismisses the panel. #}
{# @prop shown boolean Whether to display the panel statically. #}
{# @prop theme 'light'|'dark'|null The component-specific Bootstrap color mode. #}
{# @block content The offcanvas structure, typically `Offcanvas:Header` and `Offcanvas:Body`. #}
{%- props
id,
placement = 'start',
responsive = null,
backdrop = true,
scroll = false,
keyboard = true,
shown = false,
theme = null
-%}
{%- set title_id = id ~ '-label' -%}
{%- do provide('offcanvas.titleId', title_id) -%}
{%- set base_class = responsive in ['sm', 'md', 'lg', 'xl', 'xxl'] ? 'offcanvas-' ~ responsive : 'offcanvas' -%}
{%- set classes = html_classes({
(base_class): true,
('offcanvas-' ~ placement): placement in ['start', 'end', 'top', 'bottom'],
show: shown,
}) -%}
{%- set default_attrs = {
id: id,
class: classes,
tabindex: '-1',
'aria-labelledby': title_id,
} -%}
{%- if shown -%}
{%- set default_attrs = default_attrs|merge({style: 'visibility: visible; transform: none;'}) -%}
{%- endif -%}
{%- if backdrop is not same as(true) -%}
{%- set default_attrs = default_attrs|merge({'data-bs-backdrop': backdrop is same as(false) ? 'false' : 'static'}) -%}
{%- endif -%}
{%- if scroll -%}
{%- set default_attrs = default_attrs|merge({'data-bs-scroll': 'true'}) -%}
{%- endif -%}
{%- if not keyboard -%}
{%- set default_attrs = default_attrs|merge({'data-bs-keyboard': 'false'}) -%}
{%- endif -%}
{%- if theme is not null -%}
{%- set default_attrs = default_attrs|merge({'data-bs-theme': theme}) -%}
{%- endif -%}
<div {{ attributes.defaults(default_attrs) }}>
{%- block content %}{% endblock -%}
</div>
{# @block content The main content of the offcanvas. #}
<div {{ attributes.defaults({class: 'offcanvas-body'}) }}>
{%- block content %}{% endblock -%}
</div>
{# @prop closable boolean Whether to render a dismiss button. #}
{# @prop closeLabel string The accessible label of the dismiss button. #}
{# @block content The offcanvas heading, typically an `Offcanvas:Title` component. #}
{%- props closable = true, closeLabel = 'Close'|trans -%}
<div {{ attributes.defaults({class: 'offcanvas-header'}) }}>
{%- block content %}{% endblock -%}
{%- if closable %}
<button type="button" class="btn-close" data-bs-dismiss="offcanvas" aria-label="{{ closeLabel }}"></button>
{%- endif %}
</div>
{# @prop tag 'h1'|'h2'|'h3'|'h4'|'h5'|'h6' The heading element to render. #}
{# @block content The offcanvas title. #}
{%- props tag = 'h5' -%}
{%- set _offcanvas_title_id = inject('offcanvas.titleId', null) -%}
{%- set final_tag = tag in ['h1', 'h2', 'h3', 'h4', 'h5', 'h6'] ? tag : 'h5' -%}
{%- set default_attrs = _offcanvas_title_id is null
? {class: 'offcanvas-title'}
: {id: _offcanvas_title_id, class: 'offcanvas-title'}
-%}
<{{ final_tag }} {{ attributes.defaults(default_attrs) }}>
{%- block content %}{% endblock -%}
</{{ final_tag }}>
Usage
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#navigation-panel" aria-controls="navigation-panel">
Open panel
</button>
<twig:Offcanvas id="navigation-panel">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Navigation</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
Add navigation links or any other content here.
</twig:Offcanvas:Body>
</twig:Offcanvas>
Accessibility
Every trigger must identify the panel with either href or data-bs-target and expose the same id through aria-controls. The offcanvas root derives its aria-labelledby from its id, and Offcanvas:Title automatically applies the matching id, so the panel is always correctly labelled.
Bootstrap adds the dialog role and manages focus when a non-responsive offcanvas opens. Keep a visible dismiss control in the header, use descriptive trigger text, and verify that responsive panels remain understandable when they become regular page content above their breakpoint.
Examples
Offcanvas components
Display the complete panel structure statically while composing its content.
<twig:Offcanvas id="static-offcanvas" shown class="position-static w-100">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
Content for the offcanvas goes here. You can place just about any Bootstrap component or custom element here.
</twig:Offcanvas:Body>
</twig:Offcanvas>
Live demo
Open the same panel from either a link or a button.
<div class="d-flex flex-wrap justify-content-center align-items-start gap-2 w-100">
<a class="btn btn-primary" data-bs-toggle="offcanvas" href="#live-offcanvas" role="button" aria-controls="live-offcanvas">Link with href</a>
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#live-offcanvas" aria-controls="live-offcanvas">Button with data-bs-target</button>
<twig:Offcanvas id="live-offcanvas">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Content for the offcanvas goes here. You can place just about any Bootstrap component or custom element here.</p>
<div class="dropdown mt-3">
<button class="btn btn-secondary dropdown-toggle" type="button" data-bs-toggle="dropdown" aria-expanded="false">Dropdown button</button>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
</ul>
</div>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Body scrolling
Allow body scrolling without displaying a backdrop.
<div class="d-flex justify-content-center w-100">
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#scrolling-offcanvas" aria-controls="scrolling-offcanvas">Enable body scrolling</button>
<twig:Offcanvas id="scrolling-offcanvas" scroll :backdrop="false">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas with body scrolling</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Try scrolling the rest of the page while this offcanvas is open.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Body scrolling and backdrop
Keep the body scrollable while retaining the backdrop.
<div class="d-flex justify-content-center w-100">
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#scrolling-backdrop-offcanvas" aria-controls="scrolling-backdrop-offcanvas">Enable both scrolling and backdrop</button>
<twig:Offcanvas id="scrolling-backdrop-offcanvas" scroll>
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Backdrop with scrolling</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Try scrolling the rest of the page while this offcanvas is open.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Static backdrop
Require an explicit dismiss action instead of closing on backdrop clicks.
<div class="d-flex justify-content-center w-100">
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#static-backdrop-offcanvas" aria-controls="static-backdrop-offcanvas">Toggle static offcanvas</button>
<twig:Offcanvas id="static-backdrop-offcanvas" backdrop="static">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>I will not close if you click outside of me.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Dark offcanvas
Apply Bootstrap's dark color mode to the panel and its close button.
<div class="d-flex justify-content-center w-100">
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#dark-offcanvas" aria-controls="dark-offcanvas">Toggle dark offcanvas</button>
<twig:Offcanvas id="dark-offcanvas" theme="dark">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Dark offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Place offcanvas content here.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Responsive
Turn an offcanvas panel into regular content at a selected breakpoint.
<div class="w-100">
<button class="btn btn-primary d-block mx-auto d-lg-none" type="button" data-bs-toggle="offcanvas" data-bs-target="#responsive-offcanvas" aria-controls="responsive-offcanvas">Toggle offcanvas</button>
<twig:Offcanvas id="responsive-offcanvas" responsive="lg" placement="end">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Responsive offcanvas</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>This is content within an <code>.offcanvas-lg</code>.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
</div>
Placement
Open panels from any viewport edge.
<div class="d-flex flex-wrap justify-content-center align-items-start gap-2 w-100">
{% for placement in ['start', 'end', 'top', 'bottom'] %}
<button class="btn btn-primary" type="button" data-bs-toggle="offcanvas" data-bs-target="#offcanvas-{{ placement }}" aria-controls="offcanvas-{{ placement }}">Toggle {{ placement }}</button>
<twig:Offcanvas id="offcanvas-{{ placement }}" placement="{{ placement }}">
<twig:Offcanvas:Header>
<twig:Offcanvas:Title>Offcanvas {{ placement }}</twig:Offcanvas:Title>
</twig:Offcanvas:Header>
<twig:Offcanvas:Body>
<p>Offcanvas content from the {{ placement }} edge.</p>
</twig:Offcanvas:Body>
</twig:Offcanvas>
{% endfor %}
</div>
API Reference
<twig:Offcanvas>
| Prop | Type | Default |
|---|---|---|
id The unique identifier targeted by offcanvas controls.
|
string |
- |
placement The viewport edge from which the panel appears.
|
'start'|'end'|'top'|'bottom' |
'start' |
responsive The breakpoint at which the panel becomes regular content.
|
'sm'|'md'|'lg'|'xl'|'xxl'|null |
null |
backdrop Whether clicking the backdrop dismisses the panel.
|
boolean|'static' |
true |
scroll Whether the page body remains scrollable while the panel is open.
|
boolean |
false |
keyboard Whether pressing Escape dismisses the panel.
|
boolean |
true |
shown Whether to display the panel statically.
|
boolean |
false |
theme The component-specific Bootstrap color mode.
|
'light'|'dark'|null |
null |
| Block | Description |
|---|---|
content |
The offcanvas structure, typically Offcanvas:Header and Offcanvas:Body. |
<twig:Offcanvas:Body>
| Block | Description |
|---|---|
content |
The main content of the offcanvas. |
<twig:Offcanvas:Header>
| Prop | Type | Default |
|---|---|---|
closable Whether to render a dismiss button.
|
boolean |
true |
closeLabel The accessible label of the dismiss button.
|
string |
- |
| Block | Description |
|---|---|
content |
The offcanvas heading, typically an Offcanvas:Title component. |
<twig:Offcanvas:Title>
| Prop | Type | Default |
|---|---|---|
tag The heading element to render.
|
'h1'|'h2'|'h3'|'h4'|'h5'|'h6' |
'h5' |
| Block | Description |
|---|---|
content |
The offcanvas title. |