Accessible Offcanvas Side Panel in JavaScript - js-offcanvas

File Size: 25 KB
Views Total: 9814
Last Update:
Publish Date:
Official Website: Go to website
License: MIT
   
Accessible Offcanvas Side Panel in JavaScript - js-offcanvas

js-offcanvas v2 is a vanilla JavaScript offcanvas menu library that creates responsive, accessible side menu and sidebar navigation drawers for modern websites.

You can use it to build mobile-friendly navigation menus, bottom sheets, filter panels, account drawers, and admin side panels with keyboard support, ARIA states, focus trapping, focus restore, overlays, and custom positions.

Note that Version 2 is a rewritten release of the earlier jQuery plugin edition (v1). It currently uses a JavaScript class, supports module usage, and works perfectly with vanilla JavaScript projects.

Features

  • Auto-initializes panels and overlays from data attributes on DOMContentLoaded
  • 4 position options: left, right, top, and bottom.
  • Sets role="dialog" and aria-modal="true" on the panel when open.
  • Updates aria-expanded on the trigger button as the panel state changes.
  • Traps Tab and Shift+Tab focus inside the open panel.
  • Closes the panel on Escape key, except when focus is inside a form field.
  • Returns focus to the previously focused element or the trigger button after close.
  • Applies the HTML inert attribute to all background content while the panel is open.
  • Supports [data-autofocus] to control which element receives focus after the panel opens.
  • Accepts custom CSS animation names for open and close transitions.
  • Watches attribute changes with MutationObserver and re-initializes automatically.
  • Exposes public methods directly on the panel DOM element for easy programmatic control.
  • No runtime dependencies.

Use Cases

  • A mobile navigation drawer that slidesin from the left and trap keyboard focus so screen reader users can navigate the menu.
  • A product listing page adds a right-side filter panel that opens over the content, closes on overlay click, and restores scroll position and focus when dismissed.
  • An e-commerce checkout flow uses a bottom-anchored cart summary drawer that opens programmatically after the user adds an item to cart.
  • A dashboard admin panel loads a top-edge search tray with an auto-focused input field, so keyboard users land directly in the search field when the tray opens.

Alternatives:

How to use it:

1. Install the package and import the CSS and JavaScript inside your entry file.

npm install js-offcanvas
import 'js-offcanvas/dist/js-offcanvas.min.css';
import { JsOffcanvas, initOffcanvas } from 'js-offcanvas';

2. For a quick browser demo, load the CSS first and import the ESM package into the document.

<link rel="stylesheet" href="/dist/js-offcanvas.min.css">
<script type="module" src="/dist/js-offcanvas.es.js"></script>

3. Create a trigger button, an offcanvas element, and an overlay element. The trigger selector and overlay selector must match the attributes on the drawer.

<button class="site-nav-trigger" type="button">
  Menu
</button>

<aside
  id="siteNavigation"
  class="js-offcanvas"
  data-offcanvas
  button-selector=".site-nav-trigger"
  overlay-selector=".site-nav-overlay"
  position="left"
  width="19rem"
  height="100%"
  duration="0.25s"
  z-index="120"
  aria-labelledby="siteNavigationTitle">
  <nav aria-label="Primary navigation">
    <h2 id="siteNavigationTitle">Site Navigation</h2>
    <a href="/plugins/" data-autofocus>jQuery Plugins</a>
    <a href="/javascript/">JavaScript Libraries</a>
    <a href="/css/">CSS Components</a>
    <a href="/menu/">Menu Plugins</a>
    <button type="button" data-offcanvas-close>Close Menu</button>
  </nav>
</aside>

<div class="js-offcanvas-overlay site-nav-overlay" data-offcanvas-overlay></div>

4. The library automatically initializes matching elements on page load. You can call `initOffcanvas()` manually when your project loads the module after the markup.

import { JsOffcanvas, initOffcanvas } from 'js-offcanvas';

const instance = new JsOffcanvas(document.querySelector('#siteNavigation'));
initOffcanvas();

5. Set all options as HTML attributes on the <aside> or panel element. There is no JS options object.

  • position (string): Panel slide-in direction. Accepts left, right, top, or bottom. Defaults to left.
  • width (string): Panel width as a CSS value, such as 280px, 18rem, or 30vw. Defaults to 18rem.
  • height (string): Panel height as a CSS value. Defaults to 100%. Mainly useful for top and bottom positions.
  • duration (string): Transition or animation duration, such as 0.25s or 300ms. Defaults to 0.3s.
  • z-index (number): Stacking order for the panel. Defaults to 100.
  • button-selector (string): CSS selector pointing at the trigger button element. Required for auto-init to wire up click and keyboard events.
  • overlay-selector (string): CSS selector pointing at the overlay element. The overlay closes the panel on click.
  • animation-open (string): CSS animation name applied to the panel during the open transition, such as fadeInLeft or slideInRight. Requires a matching @keyframes rule in your stylesheet.
  • animation-close (string): CSS animation name applied during the close transition.
  • inert-selector (string): CSS selector for elements to mark inert while the panel is open. Defaults to body > :not(.js-offcanvas, [data-offcanvas], .js-offcanvas-overlay, [data-offcanvas-overlay]), which covers all top-level siblings of the panel and overlay.

6. API methods.

import {
  JsOffcanvas,
  JsOffcanvasOverlay,
  initOffcanvas
} from 'js-offcanvas';

const panelElement = document.querySelector('#siteNavigation');
const overlayElement = document.querySelector('.site-nav-overlay');

// Create or reuse controllers for matching panels and overlays.
const controllers = initOffcanvas();

// Create or reuse one offcanvas controller.
const menu = new JsOffcanvas(panelElement);

// Initialize every matching panel inside a DOM context.
const menus = JsOffcanvas.initAll(document);

// Get an existing offcanvas controller from an element.
const existingMenu = JsOffcanvas.getInstance(panelElement);

// Open the panel.
panelElement.open();

// Close the panel.
panelElement.close();

// Toggle the current panel state.
panelElement.toggle();

// Sync the trigger button's ARIA expanded state.
panelElement.setTriggerExpanded(true);

// Use this deprecated spelling only for legacy code paths.
panelElement.btnExpandend(false);

// Keep keyboard focus inside the panel.
panelElement.trapFocus();

// Release keyboard focus from the panel.
panelElement.removeTrapFocus();

// Remove event bindings and stored controller data.
panelElement.destroy();

// Initialize overlay elements inside a DOM context.
const overlays = JsOffcanvasOverlay.initAll(document);

// Get an existing overlay controller from an element.
const existingOverlay = JsOffcanvasOverlay.getInstance(overlayElement);

// Show the overlay.
overlayElement.show();

// Hide the overlay.
overlayElement.hide();

// Remove stored overlay controller data.
overlayElement.destroy();

7. Events.

const drawer = document.querySelector('#siteNavigation');

// Fires after the controller finishes setup.
drawer.addEventListener('init', () => {
  console.log('Drawer ready');
});

// Fires before the drawer opens. This event supports preventDefault().
drawer.addEventListener('beforeopen', (event) => {
  const menuLocked = document.body.dataset.navLocked === 'true';

  if (menuLocked) {
    event.preventDefault();
  }
});

// Fires after the drawer opens and focus moves into the panel.
drawer.addEventListener('open', () => {
  console.log('Drawer opened');
});

// Fires before the drawer closes. This event supports preventDefault().
drawer.addEventListener('beforeclose', (event) => {
  const editorForm = drawer.querySelector('form[data-dirty="true"]');

  if (editorForm) {
    event.preventDefault();
  }
});

// Fires after the drawer closes and focus returns.
drawer.addEventListener('close', () => {
  console.log('Drawer closed');
});

7. Event handlers

// before create$( document ).on( "beforecreate.offcanvas", function( e ){
  var dataOffcanvas = $( e.target ).data('offcanvas-component');
  console.log(dataOffcanvas);
  dataOffcanvas.onInit =  function() {
    console.log(this);
  };
});

// after create
$( document ).on( "create.offcanvas", function( e ){ } );

// when open
$( document ).on( "open.offcanvas", function( e ){ } );

// when closed
$( document ).on( "close.offcanvas", function( e ){ } );

// when the window resizes
$( document ).on( "resizing.offcanvas", function( e ){ } );

// when the trigger is clicked
$( document ).on( "clicked.offcanvas-trigger", function( e ){
    var dataBtnText = $( e.target ).text();
    console.log(e.type + '.' + e.namespace + ': ' + dataBtnText);
});

FAQs:

Q: Does js-offcanvas v2 work with jQuery projects?
A: Yes, but it does not require jQuery and does not depend on it. Load it as an ES module script tag alongside your jQuery files. The two libraries do not conflict.

Q: How do I open the panel on page load automatically?
A: Call panel.open() after the DOM loads. The safest place is inside a DOMContentLoaded listener or after a setTimeout(0) delay so the library's auto-init has already run.

Q: The panel opens but focus does not move inside it. What's wrong?
A: The library moves focus to [data-autofocus] or [autofocus] first. If neither exists, it focuses the panel element. The panel element needs tabindex="-1" to accept programmatic focus. The library adds that attribute automatically, but a CSS display: none or visibility: hidden on the panel's wrapper can block focus. Check that nothing hides the panel wrapper before it opens.

Q: Can I use this with a bundler like Vite or webpack?
A: Yes. Import from the npm package: import { JsOffcanvas, initOffcanvas } from 'js-offcanvas'. The package ships an ES module build at dist/js-offcanvas.es.js. Import the CSS separately: import 'js-offcanvas/dist/js-offcanvas.min.css'.

Q: The beforeclose event fires but preventDefault() does not stop the close. Why?
A: The beforeclose event is cancelable, but the close must be triggered through the library's own close() method or the built-in overlay/Escape key path. If you call panel.setAttribute('aria-hidden', 'true') or manipulate the DOM directly, the event system is bypassed. Always use panel.close() to trigger closes that need to be cancelable.

Changelog:

2026-06-02

  • v2

2019-10-16

  • Set correct aria-state on open/close

2019-10-15

  • Bugfix

2019-03-23

  • Bugfix

2018-01-23

  • v1.2.9: Fixed problem with CSS modifier class

2018-09-08

  • v1.2.8: Fade out background when closing

2018-08-12

  • v1.2.7: Added destroy method.

2018-05-06

  • Fixed Uncaught TypeError: Cannot read property 'activeElement' of undefined.
  • Fixed w.document is always null.

2018-02-02

  • Fixed Auto closes when textbox in offcanvas panel on android.

2017-12-23

  • version update - scrollable

2017-09-23

  • fixed dependencies.

2017-03-25

  • added missing options

This awesome jQuery plugin is developed by vmitsaras. For more Advanced Usages, please check the demo page or visit the official website.