More Modals Snippets
Drawer — Free HTML CSS JS Navigation Panel Snippet
Side Drawer · Modals · Plain HTML, CSS & JS · Live preview
What's included
Features
About this UI Snippet
Side Drawer — Left Navigation Drawer and Right Settings Panel with Slide Animation & Backdrop

A side drawer is a panel that slides in from the left or right edge of the screen when triggered, overlaying the main content with a semi-transparent backdrop. It is more prominent than a dropdown menu and less disruptive than a full modal — the standard pattern for navigation drawers on mobile web apps, settings panels in desktop apps, and secondary content panels in dashboards. This snippet provides both a left navigation drawer and a right settings panel with shared open/close logic, backdrop, ESC key support, and cubic-bezier slide animation.
The slide animation
Both drawers start at transform: translateX(-100%) (left) and translateX(100%) (right) — fully off-screen. Adding the .open class changes the transform to translateX(0). The CSS transition uses cubic-bezier(0.32, 0.72, 0, 1) — a spring-like curve with fast start and slow decelerate, matching iOS sheet physics. The backdrop simultaneously fades from transparent to rgba(0,0,0,0.35).
Left navigation drawer
The left drawer contains an application brand, a nav list with SVG icons and an active item indicator (indigo background on .active), a divider, and a user footer with gradient avatar initials. This is the standard mobile navigation pattern — the nav items are links to main app sections.
Right settings panel
The right drawer contains four setting rows, each with a label and a CSS toggle switch. The toggle uses the standard hidden checkbox + slider pattern: the checkbox state controls the slider background colour and knob position via :checked selectors. No JavaScript needed for the toggle switches — they work natively as checkboxes.
Shared open/close logic
The open(side) function accepts 'left' or 'right' as a parameter and adds .open to the matching drawer and backdrop. The close() function removes .open from all drawers and the backdrop simultaneously — so opening the left drawer and then clicking the backdrop closes it correctly. ESC key calls close() via a document keydown listener that is added on open and removed on close.
Accessibility
Both drawers use role="dialog" and aria-modal="true". For full focus trapping (required for WCAG 2.1 AA), add a focus trap — see the accessibility note in the FAQ. The drawer also supports a push-content layout: add margin-left: 280px to the main area and transition it in sync with the drawer — the standard desktop sidebar pattern.
Build with AI
Build, Understand, Optimize, and Extend It With AI
Rather than tracing every class toggle yourself, paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly how the shared closeDrawer() function safely closes whichever drawer is open without needing to know which side triggered it, and why the cubic-bezier(0.32, 0.72, 0, 1) curve on the transform transition reads as a spring rather than a linear slide. The same assistant can help you optimize it, for example checking whether the keydown listener being added and removed on every open/close cycle could ever leak if openDrawer is called twice in a row without a close between them. It's also useful for extending the drawer: ask it to add real focus trapping so Tab cycles only within the open panel, wire the settings toggles to localStorage so preferences persist across reloads, or convert the left drawer into a push layout that shifts the main content instead of overlaying it. Treat the code less like a finished artifact and more like a starting point for a conversation.
Prompt to recreate it
Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:
Build a slide-in side drawer system with both a left navigation panel and a right settings panel in plain HTML, CSS, and JavaScript with no library.
Requirements:
- Two drawer elements, one anchored to the left edge and one to the right, each starting fully off-screen via a CSS transform (translateX of -100% for the left, 100% for the right), transitioning to translateX(0) on a shared "open" class using a spring-like cubic-bezier easing curve, plus a full-screen backdrop element that fades in opacity in sync with whichever drawer opens.
- A single open function parameterized by which side to open (not two separate near-duplicate functions) that adds the open class to the matching drawer and the backdrop, and a single close function that removes the open class from every drawer and the backdrop regardless of which one is currently open, so closing never needs to know which side was active.
- Wire the backdrop's click event and every drawer's close button to call that same shared close function, and add a document-level Escape keydown listener that also calls it, but only while a drawer is actually open, adding the listener when a drawer opens and removing it when it closes so it never fires or leaks while both drawers are closed.
- Build the right drawer's settings toggles as real checkbox inputs paired with a CSS sibling-selector-driven slider (no JavaScript needed to visually reflect the checked state), so the toggle appearance is driven entirely by the native :checked pseudo-class.
- Mark both drawers with role="dialog" and aria-modal="true" for assistive technology, and structure the left drawer with a distinct header, scrollable navigation list, and a footer user section so it mirrors real dashboard navigation drawers.Step by step
How to Use
- 1Click Menu or Settings to open each drawerMenu opens the left navigation drawer. Settings opens the right settings panel. Click the backdrop, the × button, or press ESC to close either drawer.
- 2Update the left nav linksEdit the .nav-item anchor texts and href values in the left drawer. Move the .active class to the item matching the current page. Each nav item has an inline SVG icon — swap the icon paths to match your navigation icons.
- 3Update the settings togglesEdit the .setting-row span labels for each setting. The input type="checkbox" handles state natively — add checked to pre-enable a setting. Wire onchange to your settings API or localStorage for persistence.
- 4Add the user info in the footerUpdate the .user-av initials, .user-name, and .user-role text in the left drawer footer. Replace .user-av with an img element for a real avatar photo: add object-fit:cover and keep the border-radius:50%.
- 5Adjust the drawer widthChange width: 280px on .drawer to any value. For a full-width mobile drawer, add @media (max-width:480px) { .drawer { width: 100%; } }. For a narrower icon-only drawer, reduce to 60px and hide the text labels.
- 6Export in your formatClick "HTML" for a standalone file, "JSX" for a React component managing open state with useState and side with a string, or "Tailwind" for a Tailwind CSS version.
Real-world uses
Common Use Cases
Got questions?
Frequently Asked Questions
The drawer starts off-screen at transform: translateX(-100%) for left or translateX(100%) for right. Adding the .open class changes transform to translateX(0). The CSS transition: transform 0.3s cubic-bezier(0.32, 0.72, 0, 1) animates between these values. The cubic-bezier coordinates create a spring-like curve: the first control point (0.32, 0.72) causes fast initial acceleration, and the second (0, 1) causes slow final deceleration — matching the feel of native iOS sheet animations.
After opening the drawer, find all focusable elements inside: const focusable = drawer.querySelectorAll("a,button,input,[tabindex]"). Save the previously focused element: const prevFocus = document.activeElement. Focus the first element: focusable[0].focus(). On keydown Tab: if the last element is focused and Tab (not Shift+Tab), redirect focus to focusable[0]. On Shift+Tab from the first element, redirect to the last. On close, restore focus: prevFocus.focus().
Add onchange handlers to each checkbox: checkbox.addEventListener("change", () => { localStorage.setItem("setting_"+key, checkbox.checked); }). On page load, read each setting: const saved = localStorage.getItem("setting_"+key); if (saved !== null) checkbox.checked = saved === "true". For a server-side app, submit the settings form with a POST request and save to the user's account — the initial checked state is then rendered server-side.
Click "JSX" to download. Manage openSide state with useState<"left"|"right"|null>(null). Render the backdrop and drawers conditionally based on openSide. Apply the open CSS class: className={"drawer left" + (openSide === "left" ? " open" : "")}. Add a useEffect to attach and clean up the ESC keydown listener when openSide is not null. For the toggle switches, replace the native checkbox with controlled React inputs using useState for each setting.