Usage
<script src="/src/js/offcanvas-init.js"></script>
<div
class="l-center"
style="--cr-max-width: none; --cr-align: center; margin: var(--vertical-rhythm) var(--global-padding) 0 0"
>
<button class="off-canvas-btn-open theme button" style="margin-inline: auto"
>Open</button
>
</div>
<div class="off-canvas theme container">
<div
class="l-stack"
style="padding: var(--vertical-rhythm) var(--global-padding)"
>
<button class="off-canvas-btn-close theme button">Close</button>
<div>
<ul>
<li><a href="/">Link 1</a></li>
<li><a href="/">Link 2</a></li>
<li><a href="/">Link 3</a></li>
</ul>
</div>
</div>
</div>
.off-canvas {
position: fixed;
width: 100%;
z-index: 99999999;
transition: transform var(--oc-transition-duration, 100ms) ease-in;
transform: translateX(100%);
right: 0;
top: 3.9375rem;
height: 100%;
max-height: 100%;
overflow-y: scroll;
max-width: 23.4375rem;
&.-open {
transform: translateX(0);
transition: transform var(--oc-transition-duration, 200ms) ease-out;
}
}
import { disableBodyScroll, enableBodyScroll } from 'body-scroll-lock';
import {queryFocuseable, trapFocus, prefersReducedMotion} from './utilities/accessibility';
export default class OffCanvas {
classes = {
offcanvas : 'off-canvas',
btn_open : 'off-canvas-btn-open',
btn_close : 'off-canvas-btn-close',
offcanvas_open : '-open',
offcanvas_overlay : 'off-canvas-overlay'
};
transitionDurationCustomProperty = '--oc-transition-duration';
isOpen = false;
constructor(openBtnElement, offCanvasElement) {
console.log('fired');
this.offcanvas = offCanvasElement;
this.btn_open = openBtnElement;
this.btn_close = offCanvasElement.querySelector(`.${this.classes.btn_close}`);
this.document = document.documentElement;
this.btn_open.addEventListener('click', this.open.bind(this));
this.btn_close.addEventListener('click', this.close.bind(this));
document.addEventListener('click', this.clickOutsideHandler.bind(this));
document.addEventListener('keydown', this.closeOnEscapePress.bind(this));
if(prefersReducedMotion()) {
this.offcanvas.style.setProperty(this.transitionDurationCustomProperty, 0);
}
}
open() {
this.offcanvas.classList.add(this.classes.offcanvas_open);
this.document.classList.add(this.classes.offcanvas_overlay);
disableBodyScroll(this.offcanvas, { reserveScrollBarGap: true });
this.focuseable = queryFocuseable(this.offcanvas);
this.focuseable.firstFocuseable.focus();
this.boundTrapFocusHandler = this.trapFocusHandler.bind(this);
document.addEventListener('keydown', this.boundTrapFocusHandler);
this.isOpen = true;
}
close() {
this.offcanvas.classList.remove(this.classes.offcanvas_open);
this.document.classList.remove(this.classes.offcanvas_overlay);
document.removeEventListener('keydown', this.boundTrapFocusHandler);
enableBodyScroll(this.offcanvas);
this.isOpen = false;
}
clickOutsideHandler(e) {
const didNotClickOpenBtn = !e.target.classList.contains(`${this.classes.btn_open}`);
const clickedOutside = !e.target.closest(`.${this.classes.offcanvas}`);
if(this.isOpen && clickedOutside && didNotClickOpenBtn) {
this.close();
}
}
closeOnEscapePress = (e) => {
if (e.key === 'Escape' && this.isOpen) {
this.close();
}
};
trapFocusHandler(event) {
trapFocus(event, this.focuseable);
}
}
Useful for mobile navigation.
Todos
Elaborate on disabling scrolling documentation below. How to prevent layout shift when scrollbar disappears (more evident when the background content is visible)
Scrolling
User scroll should be prevented when the modal is open. This implimentation uses the body-scroll-lock package, which handles scroll locking for a variety of browsers and device types.
Accessibility Affordances
- Checks whether
prefers-reduced-motion
is enabled and sets the transition duration to 0. - Traps tab focus to the canvas content while open.
- Pressing the Escape key will close the component.