Sticky Cart Drawer — Free HTML CSS JS Snippet

Sticky Cart Drawer · Layouts · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

Transform-based slide animation — translateX(100%) to translateX(0) with a custom cubic-bezier easing keeps the motion on the GPU compositor thread for a smooth glide on any device
Synchronized backdrop dismiss pattern — a single .open class toggle drives both the drawer's slide-in and the backdrop's fade-in/click-to-dismiss behavior, the same two-element pattern behind every modal and dropdown
One recalc() function as the single source of truth — subtotal and item count are always re-derived fresh from the DOM's data-price attributes and quantity spans, so totals can never drift out of sync with what is on screen
Shared stepper buttons via data attributes — every +/ button uses one click handler distinguished only by data-step, so adding more cart rows requires zero new JavaScript
Clamped quantity logic — Math.max(1, ...) prevents quantities from dropping below 1, avoiding the classic "stuck at zero" or negative-quantity edge cases in cart UIs
Live item-count badge — the small red counter on the cart trigger updates in lockstep with the drawer's contents, giving visitors a persistent at-a-glance summary even while the drawer is closed
Pure vanilla JavaScript and CSS — no cart framework, state library, or build step; copy the HTML, CSS, and JS into any storefront page and the interaction works immediately

About this UI Snippet

How this sticky cart drawer was built — CSS transforms, a single recalc loop, and data attributes as the source of truth

Screenshot of the Sticky Cart Drawer snippet rendered live

This snippet recreates the slide-in "mini cart" panel that sits behind the cart icon on nearly every e-commerce site — click the trigger and a drawer glides in from the right edge with line items, quantity steppers, a live subtotal, and a checkout button, while a dimmed backdrop lets you tap anywhere to dismiss it. It's built with plain HTML, CSS transitions, and about 45 lines of vanilla JavaScript — no cart framework or state library required.

Sliding the drawer with a transform, not layout properties

The drawer is positioned absolute with right: 0 and starts off-screen via transform: translateX(100%). Toggling a single .open class flips that to translateX(0), and a cubic-bezier(.22,.9,.3,1) transition animates the move — the same "ease-out with a slight overshoot-then-settle" curve that gives slide-in panels a polished, weighted feel rather than a linear slide. Animating transform instead of right or width keeps the motion on the compositor thread, so it stays smooth even on slower devices. The Drag Resize Panels snippet explores a related layout-animation idea — driving panel dimensions interactively rather than on a toggle.

A backdrop that's both a dimmer and a dismiss target

A full-bleed .backdrop div sits beneath the drawer with opacity: 0 and pointer-events: none by default. Opening the drawer adds an .open class to *both* elements simultaneously — the backdrop fades to a translucent dark overlay via CSS transition and becomes clickable, so a single click listener on it doubles as the "click outside to close" interaction. This two-element, two-class pattern (panel + backdrop, each toggled by the same state change) is the standard approach behind every modal, drawer, and dropdown that needs a dismiss-on-outside-click behavior.

One recalc() function as the single source of truth

Rather than tracking running totals in separate variables that could drift out of sync, recalc() re-derives *everything* from the DOM on every change: it loops over all .cart-item elements, reads each one's data-price attribute and current .qty-val text, multiplies and sums them into a fresh subtotal and count, then writes both the formatted subtotal ($135.00) and the badge count back to the page. Each quantity button click just mutates the .qty-val text and calls recalc() again — there is no separate "cart state" object to keep in sync; the DOM *is* the state, and recalc() is the only function that ever reads or writes the totals.

Quantity steppers driven by a single data attribute

Both the and + buttons in every row share one .qty-btn class and are distinguished only by a data-step="-1" / data-step="1" attribute. One shared click handler reads btn.dataset.step, adds it to the current quantity, clamps the result to a minimum of 1 with Math.max(1, ...), and writes it back — meaning adding a fourth or fifth product to the cart requires no new JavaScript at all, just another <li class="cart-item" data-price="..."> block in the markup.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't have to reconstruct the DOM-as-state approach here from scratch in your head. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain precisely why recalc reads price and quantity fresh from the DOM on every call instead of maintaining a running subtotal variable, and how the shared data-step attribute lets one click handler serve both the plus and minus buttons. The same assistant can help you optimize it — for example whether recalc looping over every cart item on every single quantity click becomes a bottleneck with dozens of line items, or whether the drawer's open state should be reflected in an aria-hidden attribute for accessibility. It's also a fast way to extend the interaction: ask it to add a remove-item button, a quantity input the user can type into directly, or persist cart contents to localStorage across page loads. 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:

text
Build a slide-in "cart drawer" in plain HTML, CSS, and JavaScript using only CSS transforms and transitions — no animation library, no cart framework.

Requirements:
- A drawer panel positioned absolute or fixed to the right edge, hidden by default via transform: translateX(100%), with an .open class that sets translateX(0) and a cubic-bezier transition so it glides in rather than snapping.
- A full-bleed backdrop element behind the drawer, invisible and non-interactive by default (opacity 0, pointer-events none), that fades in and becomes clickable when the drawer opens — clicking it must close the drawer via the same function the explicit close button uses.
- Each cart line item must store its price in a data-price attribute on its container element and display its current quantity in a dedicated span, not in any separate JavaScript variable or array.
- Every quantity row needs two buttons sharing one CSS class, distinguished only by a data-step attribute of "1" or "-1"; wire a single click handler (attached per button or via delegation) that reads dataset.step, computes the next quantity clamped to a minimum of 1 with Math.max, and writes it back to the DOM.
- Write one recalc function that, whenever called, loops over every line item fresh, reads its data-price attribute and current quantity text, sums them into a subtotal and a total item count, and writes both the formatted subtotal and the item-count badge back to the page — call this same function after every quantity change so there is never a separately tracked running total that could drift out of sync with the DOM.
- Add a cart-trigger button that opens the drawer and a close button inside the drawer header that closes it.

Step by step

How to Use

  1. 1
    Position the drawer off-screen with a transformGive the <aside class="drawer"> position: absolute; right: 0 and transform: translateX(100%), then add a .open class rule that sets translateX(0) with a cubic-bezier transition for a smooth slide-in.
  2. 2
    Pair it with a clickable backdropAdd a full-bleed .backdrop div with opacity: 0; pointer-events: none by default, and an .open variant that fades it in and makes it clickable — toggle both elements' classes together so the backdrop always matches the drawer's state.
  3. 3
    Mark up cart items with a price data attributeGive each <li class="cart-item" data-price="42"> its price as a data-price attribute and a .qty-val span for its quantity — these two values are everything the recalculation logic needs to read.
  4. 4
    Build a single recalc() that re-derives totals from the DOMLoop over every .cart-item, read dataset.price and the current .qty-val text, sum price * qty into a subtotal and quantities into a count, then write both back to the subtotal element and the badge — call this same function after every change.
  5. 5
    Wire shared quantity-stepper buttonsGive every +/ button a shared .qty-btn class plus a data-step="1" or data-step="-1" attribute. One click handler reads dataset.step, applies Math.max(1, current + step), writes the new value, and calls recalc().
  6. 6
    Open, close, and dismiss-on-backdrop-clickAdd .open to both the drawer and backdrop on trigger click; remove it on close-button click *and* on backdrop click — giving visitors three natural ways to manage the panel's visibility.

Real-world uses

Common Use Cases

E-commerce storefronts and product pages
Give shoppers a fast "review and adjust" cart experience without navigating away from the product grid — the drawer keeps browsing context intact while letting them tweak quantities and see the subtotal update instantly.
Storefront redesigns and theme development
Use this as a working starting point for a custom Shopify, WooCommerce, or headless-commerce theme's mini-cart — the markup, animation, and recalculation logic translate directly once wired to real cart data.
Checkout funnels and upsell flows
Open the drawer automatically after an "Add to cart" action (as the demo does on load) to reinforce the action and surface a clear path to checkout — a proven pattern for reducing cart abandonment.
Learning the panel + backdrop dismiss pattern
A clean, minimal example of the two-element, shared-state-toggle structure that underlies every slide-in panel, modal, and dropdown menu — directly transferable to mobile nav drawers and filter sidebars.
Admin dashboards and internal tools
Repurpose the drawer-and-recalc structure for any "selected items" panel — bulk-action trays, notification centers, or order-review sidebars that need a live running total.
A reference for DOM-as-state architecture
See how recalc() treats the DOM itself as the source of truth rather than a separate JS state object — a lightweight approach worth understanding before reaching for heavier state-management patterns.

Got questions?

Frequently Asked Questions

Animating transform lets the browser handle the motion on the GPU compositor thread without triggering layout recalculation on every frame, which keeps the slide smooth even on lower-powered devices. Animating right, width, or margin forces the browser to recompute layout for the drawer (and potentially its siblings) on every frame, which is far more likely to stutter — especially with a long item list inside.

A full-bleed .backdrop element sits behind the drawer with pointer-events: none while closed (so it does not intercept clicks meant for the page) and pointer-events: auto plus a translucent fill once the .open class is added. A single click listener on that backdrop calls the same closeDrawer() function as the explicit close button — so "click outside" and "click the × button" both lead to the same state change.

Every quantity-button click calls recalc(), which loops over *all* .cart-item elements fresh, reads each one's data-price attribute and current quantity text, and recomputes the subtotal and item count from scratch. Because nothing is incrementally added or subtracted from a running total, there is no way for the displayed numbers to drift out of sync with the actual cart contents — the DOM is read as the single source of truth on every update.

Copy an existing <li class="cart-item" data-price="..."> block, change its thumbnail, name, price text, and — most importantly — its data-price attribute (the number recalc() actually reads). No JavaScript changes are required: the quantity buttons, recalculation, and count badge all work generically off the .cart-item/.qty-val/data-price structure, however many rows you add.

It clamps the result so a quantity can never drop below 1 by repeatedly clicking the button — preventing the classic cart bug where an item silently becomes "0 of something" or goes negative while still occupying a row. If you want a "remove item" affordance at zero, you would handle that as an explicit transition rather than letting the stepper produce a zero or negative value.

Yes — copy the HTML, CSS, and JS with the buttons on this page and use them anywhere, including commercial storefronts, with no attribution required. It is built entirely with vanilla JavaScript, native CSS transitions, and data-* attributes — no cart framework, state library, or build tooling to license or configure.