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 content. Click the “Close” button to dismiss – or click outside to dismiss.
Example 2: offcanvas panel slides in from the right:
Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.
Example 3: offcanvas panel slides in from the bottom:
Offcanvas content. Click the “Close” button to dismiss – or click outside to dismiss.
Example 4: offcanvas panel slides in from the left:
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
- 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. - 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. - 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 (keyboardenter
orspace
) to exit the modal, or they can use theescape
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. - 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.)
- 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). - Codebase offcanvas panels have
style="display: none;"
applied to them in their retracted state, by Alpine thex-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. 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.- The
transition-all-300ms
andtranslate-*
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 oroffcanvas-left |
Sets the side of the viewport that your offcanvas panel will slide in from. |
sm:offcanvas-override md:offcanvas-override orlg: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 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:
- Codebase CSS overrides (
md:offcanvas-override
etc.):- Stop the offcanvas panel’s
position: fixed
etc. happening above that breakpoint. - Remove the box shadow (if you have one).
- 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 theoffvanvas-override
breakpoint.
- Stop the offcanvas panel’s
- AlpineJS overrides:
- Set
isOpen
totrue
above that breakpoint (use the same breakpoint as you’re using in the stylesheet; in this examplemd
= 1024px default). - 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). - Stop the
x-trap.noscroll.inert
happening at and above that breakpoint.
- Set
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
:
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>