View as Markdown

Offcanvas

Build responsive sliding panels for navigation, forms, and supplementary content.

100%
Loading...
<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 &#039;start&#039;|&#039;end&#039;|&#039;top&#039;|&#039;bottom&#039; The viewport edge from which the panel appears. #}
{# @prop responsive &#039;sm&#039;|&#039;md&#039;|&#039;lg&#039;|&#039;xl&#039;|&#039;xxl&#039;|null The breakpoint at which the panel becomes regular content. #}
{# @prop backdrop boolean|&#039;static&#039; 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 &#039;light&#039;|&#039;dark&#039;|null The component-specific Bootstrap color mode. #}
{# @block content The offcanvas structure, typically `Offcanvas:Header` and `Offcanvas:Body`. #}
{%- props
    id,
    placement = &#039;start&#039;,
    responsive = null,
    backdrop = true,
    scroll = false,
    keyboard = true,
    shown = false,
    theme = null
-%}
{%- set title_id = id ~ &#039;-label&#039; -%}
{%- do provide(&#039;offcanvas.titleId&#039;, title_id) -%}
{%- set base_class = responsive in [&#039;sm&#039;, &#039;md&#039;, &#039;lg&#039;, &#039;xl&#039;, &#039;xxl&#039;] ? &#039;offcanvas-&#039; ~ responsive : &#039;offcanvas&#039; -%}
{%- set classes = html_classes({
    (base_class): true,
    (&#039;offcanvas-&#039; ~ placement): placement in [&#039;start&#039;, &#039;end&#039;, &#039;top&#039;, &#039;bottom&#039;],
    show: shown,
}) -%}
{%- set default_attrs = {
    id: id,
    class: classes,
    tabindex: &#039;-1&#039;,
    &#039;aria-labelledby&#039;: title_id,
} -%}
{%- if shown -%}
    {%- set default_attrs = default_attrs|merge({style: &#039;visibility: visible; transform: none;&#039;}) -%}
{%- endif -%}
{%- 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 scroll -%}
    {%- set default_attrs = default_attrs|merge({&#039;data-bs-scroll&#039;: &#039;true&#039;}) -%}
{%- endif -%}
{%- if not keyboard -%}
    {%- set default_attrs = default_attrs|merge({&#039;data-bs-keyboard&#039;: &#039;false&#039;}) -%}
{%- endif -%}
{%- if theme is not null -%}
    {%- set default_attrs = default_attrs|merge({&#039;data-bs-theme&#039;: theme}) -%}
{%- endif -%}
&lt;div {{ attributes.defaults(default_attrs) }}&gt;
    {%- block content %}{% endblock -%}
&lt;/div&gt;
{# @block content The main content of the offcanvas. #}
&lt;div {{ attributes.defaults({class: &#039;offcanvas-body&#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 offcanvas heading, typically an `Offcanvas:Title` component. #}
{%- props closable = true, closeLabel = &#039;Close&#039;|trans -%}
&lt;div {{ attributes.defaults({class: &#039;offcanvas-header&#039;}) }}&gt;
    {%- block content %}{% endblock -%}
    {%- if closable %}
        &lt;button type=&quot;button&quot; class=&quot;btn-close&quot; data-bs-dismiss=&quot;offcanvas&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 offcanvas title. #}
{%- props tag = &#039;h5&#039; -%}
{%- set _offcanvas_title_id = inject(&#039;offcanvas.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;h5&#039; -%}
{%- set default_attrs = _offcanvas_title_id is null
    ? {class: &#039;offcanvas-title&#039;}
    : {id: _offcanvas_title_id, class: &#039;offcanvas-title&#039;}
-%}
&lt;{{ final_tag }} {{ attributes.defaults(default_attrs) }}&gt;
    {%- block content %}{% endblock -%}
&lt;/{{ final_tag }}&gt;

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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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.

100%
Loading...
<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 
string -
placement 
'start'|'end'|'top'|'bottom' 'start'
responsive 
'sm'|'md'|'lg'|'xl'|'xxl'|null null
backdrop 
boolean|'static' true
scroll 
boolean false
keyboard 
boolean true
shown 
boolean false
theme 
'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 
boolean true
closeLabel 
string -
Block Description
content The offcanvas heading, typically an Offcanvas:Title component.

<twig:Offcanvas:Title>

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