Off-Canvas Push Menu — Free HTML CSS JS Sliding Navigation Snippet
Off-Canvas Push Menu · Navigation · Plain HTML, CSS & JS · Live preview
Copy & paste
Source Code
<div class="app" id="app">
<nav class="off-canvas" id="offCanvas" aria-hidden="true">
<div class="oc-header">
<span class="oc-brand">Acme Inc.</span>
<button class="oc-close" onclick="closeMenu()" aria-label="Close menu">×</button>
</div>
<a href="#" class="oc-link">Home</a>
<a href="#" class="oc-link">Products</a>
<a href="#" class="oc-link">Pricing</a>
<a href="#" class="oc-link">About</a>
<a href="#" class="oc-link">Contact</a>
</nav>
<div class="page-wrap" id="pageWrap">
<header class="topbar">
<button class="hamburger" id="hamburger" onclick="toggleMenu()" aria-label="Open menu" aria-expanded="false" aria-controls="offCanvas">
<span></span><span></span><span></span>
</button>
<span class="brand">Acme Inc.</span>
</header>
<main class="content">
<h1>Welcome back</h1>
<p>This is the page content. When the menu opens, this entire wrapper slides sideways on the X axis instead of a drawer sliding over it — a push-style off-canvas layout.</p>
<div class="card">Some page content block</div>
<div class="card">Another content block</div>
</main>
</div>
<div class="scrim" id="scrim" onclick="closeMenu()"></div>
</div>* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; }
.app {
position: relative;
overflow-x: hidden;
min-height: 100vh;
background: #0f172a;
}
.off-canvas {
position: absolute;
top: 0;
left: 0;
width: 240px;
height: 100%;
background: #0f172a;
color: #e2e8f0;
padding: 20px 0;
transform: translateX(-100%);
transition: transform 0.3s ease;
z-index: 1;
}
.oc-header {
display: flex;
align-items: center;
justify-content: space-between;
padding: 0 20px 16px;
border-bottom: 1px solid #1e293b;
margin-bottom: 12px;
}
.oc-brand { font-weight: 700; font-size: 15px; }
.oc-close {
background: none;
border: none;
color: #94a3b8;
font-size: 22px;
line-height: 1;
cursor: pointer;
}
.oc-link {
display: block;
padding: 12px 20px;
color: #cbd5e1;
text-decoration: none;
font-size: 14px;
font-weight: 500;
transition: background 0.15s, color 0.15s;
}
.oc-link:hover { background: #1e293b; color: #fff; }
.page-wrap {
position: relative;
background: #f8fafc;
min-height: 100vh;
transition: transform 0.3s ease;
z-index: 2;
}
.page-wrap.shifted { transform: translateX(240px); }
.topbar {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 20px;
background: #fff;
border-bottom: 1px solid #e2e8f0;
}
.hamburger {
width: 34px;
height: 34px;
border: none;
background: #f1f5f9;
border-radius: 8px;
cursor: pointer;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
}
.hamburger span {
width: 16px;
height: 2px;
background: #1e293b;
border-radius: 2px;
transition: transform 0.25s, opacity 0.25s;
}
.hamburger[aria-expanded="true"] span:nth-child(1) { transform: translateY(7px) rotate(45deg); }
.hamburger[aria-expanded="true"] span:nth-child(2) { opacity: 0; }
.hamburger[aria-expanded="true"] span:nth-child(3) { transform: translateY(-7px) rotate(-45deg); }
.brand { font-weight: 700; font-size: 15px; color: #1e293b; }
.content { padding: 28px 20px; max-width: 480px; }
.content h1 { font-size: 22px; color: #1e293b; margin: 0 0 10px; }
.content p { font-size: 14px; color: #64748b; line-height: 1.6; margin: 0 0 16px; }
.card {
background: #fff;
border: 1px solid #e2e8f0;
border-radius: 10px;
padding: 16px;
margin-bottom: 12px;
font-size: 13px;
color: #475569;
}
.scrim {
position: fixed;
inset: 0;
background: rgba(15, 23, 42, 0.35);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 1;
}
.scrim.visible { opacity: 1; pointer-events: auto; }function toggleMenu() {
const menu = document.getElementById('offCanvas');
const wrap = document.getElementById('pageWrap');
const hamburger = document.getElementById('hamburger');
const scrim = document.getElementById('scrim');
const isOpen = menu.classList.contains('open');
if (isOpen) {
closeMenu();
} else {
menu.classList.add('open');
menu.style.transform = 'translateX(0)';
wrap.classList.add('shifted');
scrim.classList.add('visible');
hamburger.setAttribute('aria-expanded', 'true');
menu.setAttribute('aria-hidden', 'false');
}
}
function closeMenu() {
const menu = document.getElementById('offCanvas');
const wrap = document.getElementById('pageWrap');
const hamburger = document.getElementById('hamburger');
const scrim = document.getElementById('scrim');
menu.classList.remove('open');
menu.style.transform = 'translateX(-100%)';
wrap.classList.remove('shifted');
scrim.classList.remove('visible');
hamburger.setAttribute('aria-expanded', 'false');
menu.setAttribute('aria-hidden', 'true');
}Extend with AI
Take This Snippet Further With AI
The AI prompt for this one is for Patreon supporters ($3 a month, 7 days free), along with a site with no ads. Supporters enter the passcode once and it is remembered in this browser.
What's included
Features
About this UI Snippet
Off-Canvas Push Menu — HTML, CSS & JavaScript Sliding Page Navigation

Most mobile navigation drawers are overlays: a panel slides in on top of the page while the page itself stays put. A push-style off-canvas menu is different — the entire visible page shifts sideways on the X axis to reveal a hidden sidebar underneath it. It's the pattern used by older Facebook and many native-feeling mobile web apps because it makes the menu feel like a physical layer beneath the content rather than a modal floating above it.
This snippet builds that effect with plain HTML, CSS, and vanilla JavaScript. There are three sibling elements inside .app: the .off-canvas sidebar positioned absolutely at the left edge, the .page-wrap that contains the topbar and all page content, and a .scrim overlay used only to catch outside clicks. No layout library, no drawer component, no npm package.
How the push effect works
The trick is that .off-canvas starts at transform: translateX(-100%) — fully hidden off the left edge of the viewport — while .page-wrap sits on top of it at transform: translateX(0). When the menu opens, two things happen at once: the off-canvas panel animates to translateX(0) to slide into view, and .page-wrap gets a .shifted class that animates it to transform: translateX(240px) — the exact width of the sidebar. Because both elements share the same 0.3s ease transition, the page appears to be *pushed* to the right by the menu sliding in underneath it, rather than the menu sliding on top of the page. The parent .app has overflow-x: hidden so the page-wrap's rightward shift never creates a horizontal scrollbar.
How the hamburger-to-X animation works
The hamburger icon is three <span> bars inside a button. When aria-expanded="true" is set on the button, three sibling selectors kick in: the top bar translates down 7px and rotates 45deg, the middle bar fades to opacity: 0, and the bottom bar translates up 7px and rotates -45deg. Together the two remaining bars form an X. Driving this off the aria-expanded attribute rather than a separate class means the visual state and the accessibility state can never drift out of sync.
Closing the menu
Three things close the menu: clicking the close button inside the panel, clicking the semi-transparent scrim that covers the page while the menu is open, or clicking the hamburger again. All three call the same closeMenu() function, which resets the transforms, removes the .shifted and .visible classes, and flips aria-expanded and aria-hidden back.
Changing the sidebar width
The sidebar width is set in exactly two places that must match: .off-canvas { width: 240px } and .page-wrap.shifted { transform: translateX(240px) }. Change both values together — if they diverge, the page will either overshoot the sidebar's edge or leave a gap.
Step by step
How to Use
- 1Load the snippetClick "Off-Canvas Push Menu" in the sidebar Library tab to load the HTML, CSS, and JS panels and see the closed state in the preview.
- 2Open the menuClick the hamburger icon in the preview. Watch the page content shift right as the dark sidebar slides in from underneath it, and the hamburger morph into an X.
- 3Close it three waysClick the X inside the sidebar, click the dimmed scrim area, or click the hamburger again — all three should close the menu identically.
- 4Resize the sidebarIn the CSS panel, change both the width on .off-canvas and the translateX value on .page-wrap.shifted to the same new value, then reopen the menu to confirm the page shifts exactly that far.
- 5Add or edit linksIn the HTML panel, add more .oc-link anchors inside the nav — the flex layout and hover states apply automatically to any number of links.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for React, or "Tailwind" for a React + Tailwind version. "Copy all" copies everything to the clipboard.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
It is a navigation pattern where opening the menu shifts the entire visible page sideways to reveal a hidden sidebar underneath it, instead of sliding a drawer on top of the page. Both the sidebar and the page move using CSS transforms so they appear physically connected.
A normal drawer is an overlay: it slides in above the page using a higher z-index while the page stays fixed in place and is often dimmed with a scrim. Here, the page itself moves via transform: translateX, so the menu feels like it lives beneath the content and pushes it aside rather than covering it.
Update the width value on .off-canvas and the translateX distance on .page-wrap.shifted to the same new number — for example 280px in both places. They must match exactly or the page will either overshoot the sidebar edge or leave a visible gap.
The outer .app container has overflow-x: hidden, which clips anything that extends past the viewport width during the transform animation, including the momentary overhang while the page-wrap is sliding.
The three bars are span elements targeted with nth-child selectors that key off the button's aria-expanded attribute. When it is true, the top and bottom bars rotate 45 and -45 degrees and translate toward the center, and the middle bar fades out, forming an X shape purely with CSS transitions.
Yes. Move .off-canvas to right: 0 instead of left: 0, change its initial transform to translateX(100%), and change .page-wrap.shifted to translateX(-240px) so the page shifts left instead of right.
It works at any viewport width, but the push effect is most natural on narrower screens where a permanent sidebar would not fit. On wide desktop layouts you may prefer a permanently visible sidebar instead of a toggled one.
Yes. Add a details/summary element or a small accordion pattern in place of an .oc-link anchor. The sidebar's fixed width and vertical layout accommodate nested content as long as you manage its own expand/collapse state separately from the outer menu.
Yes. While the Off-Canvas Push Menu snippet gives you one pattern to copy, the free Responsive Navbar Builder at webdevpuneet.com/navbar-builder/ designs a whole navigation bar: logo, links and dropdown menus, a mobile hamburger menu (dropdown panel, slide-in drawer or full screen) and a scroll effect (sticky, shrink, hide on scroll or transparent). It has six presets, a live preview that runs the real code, and exports HTML + CSS + vanilla JS, React, Tailwind, Vue 3 or Angular — or you can click Fork & Edit to keep editing it in My Code.