Bismillâh. A cart drawer is the most common "panel slides in from
the edge" pattern on the web. Shopify, Amazon, every Shopify theme
you've ever cloned. It feels like something a framework should do,
but it's two transforms, one transition,
and four event listeners. Twenty lines of JS. Let's build one
that feels native, works on touch, and respects
prefers-reduced-motion.
The architecture
Two elements live off-screen until you open the drawer:
-
A backdrop: a full-screen fixed div at
opacity: 0; pointer-events: none. Click-through until we activate it. -
The drawer itself: a fixed aside pinned to the
right edge, translated off by 100% of its own width with
transform: translateX(100%).
Adding the .open class on both flips the backdrop to
opacity: 1; pointer-events: auto and the drawer to
transform: translateX(0). That's the whole
animation.
The CSS
.backdrop {
position: fixed;
inset: 0;
background: rgba(10, 5, 20, 0.6);
backdrop-filter: blur(2px);
opacity: 0;
pointer-events: none;
transition: opacity 0.3s ease;
z-index: 40;
}
.backdrop.open { opacity: 1; pointer-events: auto; }
.drawer {
position: fixed;
top: 0; right: 0; bottom: 0;
width: min(360px, 100vw);
transform: translateX(100%);
transition: transform 0.32s cubic-bezier(0.22, 0.61, 0.36, 1);
z-index: 50;
}
.drawer.open { transform: translateX(0); }
Three careful choices worth naming:
-
width: min(360px, 100vw). On a phone, the drawer is full-width. On a desktop, it's fixed at 360 px. One line, no media query. -
Easing
cubic-bezier(0.22, 0.61, 0.36, 1)is the "ease-out-expo-ish" curve: fast out of the gate, settles softly. This is the single most useful easing for UI motion, because it makes everything feel "placed" rather than "tossed." -
backdrop-filter: blur(2px). Two pixels of blur on the backdrop takes your eye off the main page without shouting. Any more and the page behind feels washed out. Any less and the depth reads as a flat overlay.
The JavaScript
const drawer = document.querySelector('#drawer')
const backdrop = document.querySelector('#backdrop')
function open() {
drawer.classList.add('open')
backdrop.classList.add('open')
document.body.style.overflow = 'hidden'
}
function close() {
drawer.classList.remove('open')
backdrop.classList.remove('open')
document.body.style.overflow = ''
}
document.querySelector('#openCart').addEventListener('click', open)
document.querySelector('#closeCart').addEventListener('click', close)
backdrop.addEventListener('click', close)
document.addEventListener('keydown', e => {
if (e.key === 'Escape' && drawer.classList.contains('open')) close()
})
Four entry points for closing the drawer:
- The explicit "✕" close button.
- Clicking the backdrop (universal mobile app convention).
- Pressing Escape on a keyboard (universal desktop convention).
- Opening any other overlay, if you have more drawers later.
The document.body.style.overflow = 'hidden' line
locks the background from scrolling while the drawer is open. On
a phone, this stops the page behind the drawer from bouncing
when the user scrolls inside the cart list.
Accessibility: five lines that matter
<aside class="drawer" aria-hidden="true" aria-label="Shopping cart">
-
aria-hiddentoggles between"true"(closed) and"false"(open). Screen readers skip the drawer entirely when closed. -
aria-label="Shopping cart"names the region. When focus moves inside, the reader announces "shopping cart" so the user knows where they landed. -
The close button carries its own
aria-label="Close cart"because the visible label "✕" doesn't read well on its own.
In a larger production version, trap focus inside the drawer
(tabbing should cycle through cart elements, not escape to the
page behind). Libraries like focus-trap handle this
in three lines; for a demo, the basic ARIA above is already more
accessible than most e-commerce sites.
Mobile drag-to-close (optional)
On a real mobile cart, users expect to drag the drawer off-screen
to close it. That's 30 more lines of JavaScript using
pointerdown/pointermove/pointerup. For this post,
the backdrop-tap and the close button are enough. We'll add
drag-to-close in a later post when we talk about bottom sheets
and iOS-style presentations.
Tricks worth carrying
- Always lock the body scroll while a modal or drawer is open. Without this, the background scrolls when the user scrolls inside the cart. Feels broken on touch.
-
Transitions on
transformare GPU-accelerated. Animatingright: -360pxtoright: 0triggers layout on every frame.translateXdoesn't. Prefer transforms for any motion. - z-index: keep a small, documented stack. Backdrop at 40, drawer at 50, toast at 60, modal at 70. Don't reach for 9999; you'll regret it.
Takeaways
- Two elements (backdrop + drawer), two classes, one transition.
- Four ways to close: button, backdrop, Escape, programmatic.
- ARIA labels turn a visual flourish into accessible UI.
- Animate transforms, never layout properties.
Next post: "Quantity inputs that don't break, using Intl.NumberFormat."
Slide cleanly, kardeşim. Every motion should settle.