Skip to main content
AlpineJS components

Offcanvas

Codebase’s offcanvas panels can slide in from any side of the viewport (top, right, bottom, or left). They can be dismissed by a close button and/or by clicking outside. Their control button can be within the Alpine component, or in a separate component. Also, offcanvas panels can be overridden, so that they become like a normal (on-canvas) panel within the document flow above a breakpoint of your choice.

Offcanvas from four sides

Example 1: offcanvas panel slides in from the top:

Offcanvas 1 Panel Title

Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.

Example 2: offcanvas panel slides in from the right:

Offcanvas 2 Panel Title

Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.

Example 3: offcanvas panel slides in from the bottom:

Offcanvas 3 Panel Title

Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.

Example 4: offcanvas panel slides in from the left:

Offcanvas 4 Panel Title

Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.

<div x-data="{ isOpen: false }">
<button
class="btn-primary"
@click="isOpen = true"
aria-label="Offcanvas example 4"
aria-controls="offcanvas-ex-4"
:aria-expanded="isOpen"
>
Open offcanvas</button>
<div
class="fixed box z-index-999"
x-show="isOpen"
@click="isOpen = false"
>

<div
id="offcanvas-ex-4"
aria-labelledby="offcanvas-ex-4-title"
x-cloak
x-show="isOpen"
x-transition:enter="transition-all-300ms"
x-transition:enter-start="translate-left-100%"
x-transition:enter-end="translate-0"
x-transition:leave="transition-all-300ms"
x-transition:leave-start="translate-0"
x-transition:leave-end="translate-left-100%"
x-trap.noscroll.inert="isOpen"
class="offcanvas offcanvas-left w-xxs overflow-y bs-1 p-2 bg-white"
@click.stop
@keyup.escape="isOpen = false"
>

<div class="mb-2 t-right">
<button
class="btn-icon btn-sm rounded-full"
aria-label="Close offcanvas panel"
@click="isOpen = false">

<!-- Icon close x -->
</button>
</div>
<div class="h3" id="offcanvas-ex-4-title">Offcanvas 4 Panel Title</div>
<p>Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.</p>
<nav>
<ul class="menu">
<li><a class="py-1" href="#/">Example link 1</a></li>
<li><a class="py-1" href="#/">Example link 2</a></li>
<li><a class="py-1" href="#/">Example link 3</a></li>
</ul>
</nav>
</div>
</div>
</div>

Notes on offcanvas

  1. The width of offcanvas right, or offcanvas left, can be whatever you need, up to 100vw. You can set it by utility classes. Also, add overflow-y if required.
  2. The height of offcanvas top, or offcanvas bottom, can be whatever you need, up to 100vh. You can set it by utility classes. Also, add overflow-y if required.
  3. Offcanvas panels require the AlpineJS Focus plugin for keeping the focus on the offcanvas-panel when it is expanded. x-trap puts the focus on the first focusable element inside the offcanvas panel. Keyboard users can click the “Close” button (keyboard enter or space) to exit the modal, or they can use the escape key. When the offcanvas panel is expanded, x-trap puts the focus on the first actionable item in the panel – it is good practive to make this the close button, as per these examples.
  4. Offcanvas panels have a full-cover backdrop that is the parent element of the offcanvas panel. This backdrop prevents anything else on the page being clicked by accident while the offvanvas is open.
<div
class="fixed box z-index-999"
x-show="isOpen"
@click="isOpen = false"
>

<!-- The offcanvas panel goes in here -->
</div>

In all these examples, no background color (and no glass effect) has been set for the backdrop, therefore it is invisible. For handling “click outside to dismiss”, @click="isOpen = false" is placed on the backdrop. All offcanvas examples in these docs have this, but it is optional – you can do without it if your design requires it to be so. (Note: if you use @click="isOpen = false" on the backdrop, then you need to have @click.stop on the offcanvas panel to stop clicks on the panel bubbling up and clicking the @click="isOpen = false" too.)

  1. Offcanvas panels must have a title. I have styled offcanvas titles using the h3 utility class in a <div> (instead of using the <h3> HTML tag), making the titles are obvious for sighted people but not upsetting the heading hierarchy (which could mislead people who are reliant on screen readers, and it is bad for SEO).
  2. Codebase offcanvas panels have style="display: none;" applied to them in their retracted state, by Alpine the x-show directive. This means that thet are not included in the accessibility tree (tab index) while retracted. And it means that if you have a box shadow on the offcanvas panel, it will not be seen protruding from the edge of the viewport.
  3. offcanvas-top, offcanvas-right etc. set the initial retracted position styles for the offcanvas panel. They don’t handle its animation. You can override these styles above particular breakpoints, using e.g. sm:offcanvas-override. This will allow your panel to behave as a normal panel above that breakpoint.
  4. The transition-all-300ms and translate-* classes are all applied using Alpine 3’s x-transition directive.

Offcanvas classes

There are only four HTML elements required for the basic offcanvas skeleton (including the AlpineJS component wrapping <div>)"

<div x-data="{ isOpen: false }">
<button>Click to reveal</button>

<!-- The offcanvas panel -->
<div class="offcanvas">
<button>Click to close</button>
Offcanvas panel content.
</div>

</div><!-- End of Alpine component -->

As descibed previously, the control button can be in its own separate Alpine component if your design requires it to be so. Then, use the offcanvas panel’s side classes to set what side the panel slides in from, and its z-index layer above the webpage. Then you have the option to add another class that overrides the offcanvas panel at a particular breakpoint – so that it reverts to being

The Codebase offcanvas CSS classes are as follows:

CSS class Explanation
offcanvas Fixes the position of the offcanvas panel in its unretracted state, and its z-index layer above the webpage.
offcanvas-top
offcanvas-right,
offcanvas-bottom or
offcanvas-left
Sets the side of the viewport that your offcanvas panel will slide in from.
sm:offcanvas-override
md:offcanvas-override or
lg:offcanvas-override
Overrides the offvanvas panel’s styling, so that it is displayed as a normal panel within the document flow.
Use utility classes to set the panel’s width, box shadow, padding, and background color.
transition-all-300ms Used by the AlpineJS x-transition directive, this sets the CSS transition for the offcanvas panel.
translate-up-100%
translate-right-100%
translate-bottom-100% or translate-right-100%
Used by AlpineJS x-transition directive, these set the retracted position of the panel by CSS transform.
translate-0 Used by the AlpineJS x-transition directive, this sets the expanded position of the panel by CSS transition, necessary to undo the retracted position. (The same translate-0 class works for top, right, bottom or left.)

Button as a separate component

Until Alpine v.3, having a toggler or button as a separate component was possible using the Alpine $dispatch magic property and the window as an event bus.

But Alpine 3 has built-in global state storage, using Alpine.store(). So, we can use that instead.

Example 5: with a control button in a separate Alpine component:

Offcanvas 5 Panel Title

Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.

<!-- The isOpen (state) store -->
<script>
document.addEventListener('alpine:init', () => {
Alpine.store('offcanvasEx5', {
isOpen: false
})
})
</script>

<!-- The (separate) button component -->
<div x-data>
<button
class="btn-primary"
@click="$store.offcanvasEx5.isOpen = true"
aria-label="Offcanvas example 5"
aria-controls="offcanvas-ex-5"
:aria-expanded="$store.offcanvasEx5"
@keydown.escape="$store.offcanvasEx5.isOpen = false">
Open offcanvas</button>
</div>

<!-- The offcanvas component (without button) -->
<div
x-data
class="fixed box z-index-999"
x-show="$store.offcanvasEx5.isOpen"
@click="$store.offcanvasEx5.isOpen = false"
>

<div
id="offcanvas-ex-5"
aria-labelledby="offcanvas-ex-5-title"
x-cloak
x-show="$store.offcanvasEx5.isOpen"
x-transition:enter="transition-all-300ms"
x-transition:enter-start="translate-up-100%"
x-transition:enter-end="translate-0"
x-transition:leave="transition-all-300ms"
x-transition:leave-start="translate-0"
x-transition:leave-end="translate-up-100%"
x-trap.noscroll.inert="$store.offcanvasEx5.isOpen"
class="offcanvas offcanvas-top w-100% overflow-y bs-1 p-2 bg-white"
@click.stop
@keyup.escape="$store.offcanvasEx5.isOpen = false"
>

<div class="mb-2 t-right">
<button
class="btn-icon btn-sm rounded-full"
aria-label="Close offcanvas panel"
@click="$store.offcanvasEx5.isOpen = false">

<!-- Icon close x -->
</button>
</div>
<div class="h3" id="offcanvas-ex-5-title">Offcanvas 5 Panel Title</div>
<p>Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.</p>
<nav>
<ul class="menu">
<li><a class="py-1" href="#/">Example link 1</a></li>
<li><a class="py-1" href="#/">Example link 2</a></li>
<li><a class="py-1" href="#/">Example link 3</a></li>
</ul>
</nav>
</div>
</div>

Offcanvas override (for wide viewports)

To override Codebase offcanvas above any of the media query breakpoints, we need the following:

  1. Codebase CSS overrides (md:offcanvas-override etc.):
    1. Stop the offcanvas panel’s position: fixed etc. happening above that breakpoint.
    2. Remove the box shadow (if you have one).
    3. If your offcanvas panel slides in from the right or left, then you need to put md:w-auto (or other) on the panel to override its width at the offvanvas-override breakpoint.
  2. AlpineJS overrides:
    1. Set isOpen to true above that breakpoint (use the same breakpoint as you’re using in the stylesheet; in this example md = 1024px default).
    2. Stop the @click.outside and @keyup.escape dismissers happening at and above that breakpoint (because you don’t want the panel to disappear while it is behaving as a normal on-canvas panel).
    3. Stop the x-trap.noscroll.inert happening at and above that breakpoint.

So, in the Alpine.store() data you want the isOpen state to initialize as false below the breadpoint (the offcanvas panel is retracted) but as true at and above the breakpoint (so that it can behaves as a normal visible panel).

Example 6: with control button as a separate Alpine component, and with offcanvas override. Below the md breakpoint (1024px default), the offcanvas panel slides in from the top. At or above md it behaves as a normal (on canvas) panel. And the control button and the close button are both hidden a or above md:

Offcanvas 6 Panel Title

This panel will behave as a normal panel at and above the md breakpoint (1024px default). And it will behave as an offcanvas panel below md.

Note: the offcanvas-override class is required on both the outer (component) <div> and the moving part (offcanvas panel) <div>.

<!-- The state store -->
<script>
document.addEventListener('alpine:init', () => {
let windowWidth = window.innerWidth;
Alpine.store('offcanvasEx6', {
isOpen: window.innerWidth >= 1024,
isBelowBP: window.innerWidth < 1024,
reset() {
let currentWidth = window.innerWidth;
if (currentWidth != windowWidth) {
windowWidth = currentWidth;
if (windowWidth >= 1024) {
this.isOpen = true,
this.isBelowBP = false
} else {
this.isOpen = false,
this.isBelowBP = true
}
}
}
})
})
</script>

<!-- The (separate) button component -->
<div
x-data
@resize.window.debounce="$store.offcanvasEx6.reset()"
>

<button
class="btn-primary"
@click="$store.offcanvasEx6.isOpen = true"
aria-label="Offcanvas example 6"
aria-controls="offcanvas-ex-6"
:aria-expanded="$store.offcanvasEx6"
x-show="$store.offcanvasEx6.isBelowBP"
>
Open offcanvas</button>
</div>

<!-- The offcanvas component (without button) -->
<div
x-data
class="fixed box z-index-999 md:offcanvas-override"
x-show="$store.offcanvasEx6.isOpen"
@click="$store.offcanvasEx6.isOpen = false"
>

<div
id="offcanvas-ex-6"
aria-labelledby="offcanvas-ex-7-title"
x-cloak
x-show="$store.offcanvasEx6.isOpen"
x-transition:enter="transition-all-300ms"
x-transition:enter-start="translate-up-100%"
x-transition:enter-end="translate-0"
x-transition:leave="transition-all-300ms"
x-transition:leave-start="translate-0"
x-transition:leave-end="translate-up-100%"
x-trap.noscroll.inert="$store.offcanvasEx6.isOpen && $store.offcanvasEx6.isBelowBP"
class="offcanvas offcanvas-top md:offcanvas-override w-100% overflow-y bs-1 md:b-thick p-2 bg-white"
@click.stop
@keyup.escape="$store.offcanvasEx6.isOpen = !$store.offcanvasEx6.isBelowBP || false"
>

<div
class="mb-2 t-right"
x-show="$store.offcanvasEx6.isBelowBP"
>

<button
class="btn-icon btn-sm rounded-full"
aria-label="Close offcanvas panel"
@click="$store.offcanvasEx6.isOpen = false">

<!--icon close x -->
</button>
</div>
<div class="h3" id="offcanvas-ex-6-title">Offcanvas 6 Panel Title</div>
<p>This panel will behave as a normal panel at and above the
<code class="b-thin">md</code> breakpoint (1024px default).
And it will behave as an offcanvas panel below
<code class="b-thin">md</code>.</p>
<nav>
<ul class="menu">
<li><a class="py-1" href="#/">Example link 1</a></li>
<li><a class="py-1" href="#/">Example link 2</a></li>
<li><a class="py-1" href="#/">Example link 3</a></li>
</ul>
</nav>
</div>
</div>