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 |
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"andaria-modal="true"on the panel when open. - Updates
aria-expandedon 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
inertattribute 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
MutationObserverand 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:
- Stackable Multi-level Sidebar Menu - hc-offcanvas-nav
- 10 Best Off-canvas Mobile Menus In JavaScript And CSS
- Multi-level Side Navigation For React
- React Bottom Sheet and Off-Canvas Drawer Component – Hiraki
- Build Customizable Sidebars For Next.js – shadcn/ui sidebar
- Responsive, Expandable, Retractable Sidebar Component with Shadcn/UI
- VS Code-Style Shadcn/ui Resizable Sidebar for Next.js Apps
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. Acceptsleft,right,top, orbottom. Defaults toleft.width(string): Panel width as a CSS value, such as280px,18rem, or30vw. Defaults to18rem.height(string): Panel height as a CSS value. Defaults to100%. Mainly useful for top and bottom positions.duration(string): Transition or animation duration, such as0.25sor300ms. Defaults to0.3s.z-index(number): Stacking order for the panel. Defaults to100.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 asfadeInLeftorslideInRight. Requires a matching@keyframesrule in your stylesheet.animation-close(string): CSS animation name applied during the close transition.inert-selector(string): CSS selector for elements to markinertwhile the panel is open. Defaults tobody > :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.











