Mini Cart Dropdown — Free HTML CSS JS Snippet

Mini Cart · Cards · Plain HTML, CSS & JS · Live preview

Share & Support

What's included

Features

.open class toggle: display none/flex switch for dropdown visibility
position: absolute anchored right: 0 with z-index: 100 layering
Gradient thumbnail divs: no image dependencies, scalable to any product
Qty stepper: Math.max(1) guard ensures quantity never drops to zero
data-price attribute: unit price stored in DOM for state-free calculation
btn.closest(".cart-item") remove: clean DOM traversal without IDs
Free shipping badge with SVG truck icon and green accent
Subtotal/total recalculated on every qty change and item removal
Checkout and continue shopping CTAs with distinct visual hierarchy

About this UI Snippet

Mini Cart Dropdown — Item List, Quantity Stepper, Subtotal & Checkout UI

Screenshot of the Mini Cart snippet rendered live

A mini cart (also called a cart flyout) is a compact shopping cart summary that appears on click, anchored to the cart icon in the header. It lets users review items, adjust quantities, and proceed to checkout without navigating away from the current page. This snippet provides a complete mini cart dropdown with gradient product thumbnails, quantity steppers, remove buttons, a free shipping indicator, subtotal/total calculations, and a button that leads to the checkout form.

The dropdown toggle and positioning

The cart dropdown uses position: absolute anchored to the .cart-wrap container. It is display: none by default and switches to display: flex on toggle via the .open class. On toggle, the JS adds or removes .open from the dropdown element. .cart-wrap uses position: relative as the containing block. The right: 0 keeps the dropdown flush with the cart button on the right side.

Quantity stepper controls

Each .cart-item has a .qty-ctrl row with two buttons and a span showing the quantity. changeQty() increments or decrements the span text, enforcing Math.max(1, ...) so quantity never drops below 1. After every change, updateSummary() re-reads all item quantities and prices from the DOM — each .cart-item has a data-price attribute holding the unit price as a float string.

Remove item flow

btn.closest('.cart-item') traverses up the DOM to the containing item row and removes the entire element. updateSummary() then re-counts remaining items, updates the badge count and item count in the header, and recalculates the total.

Price calculation without state

getTotal() iterates all .cart-item elements, reading parseFloat(item.dataset.price) and multiplying by the qty span. This data-attribute approach avoids a separate JS state object for the demo. In production, keep a cart array in memory (or localStorage) and recalculate from that rather than reading the DOM.

Accessibility and keyboard support

The cart button is a native button element, giving it automatic keyboard focus and Enter/Space activation. The close button inside the dropdown is also a button, so keyboard users can Tab into the dropdown and close it without a mouse. For a production implementation, add aria-expanded="true/false" to the cart button and aria-modal="true" with role="dialog" to the dropdown so screen readers announce the panel state correctly. Trap focus inside the dropdown when open using a focusTrap utility so Tab does not cycle behind the overlay.

Animation and transition considerations

The current snippet uses a hard display: none / display: flex toggle. To animate the dropdown open and close, replace the display switch with an opacity and transform transition: set opacity: 0; transform: translateY(-8px); pointer-events: none on the closed state, and opacity: 1; transform: translateY(0); pointer-events: auto on .open. CSS transitions on these properties create a smooth slide-down effect without JavaScript animation loops.

Build with AI

Build, Understand, Optimize, and Extend It With AI

You don't need to trace the DOM reads by hand to see how this stays in sync. Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why getTotal reads price and quantity straight from data-price attributes and .qty text instead of a separate cart array, or what would break the moment two cart items shared the same data-price value. The same assistant can help optimize it — asking whether re-scanning every .cart-item on each quantity change scales past a few dozen line items, or whether the mini cart should move to a single source-of-truth cart object once persistence is added. It's just as useful for extending the feature: ask it to add a free-shipping progress bar that reuses updateSummary, close the dropdown on outside click and Escape, or persist the cart to localStorage across page reloads. 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 "mini cart" flyout dropdown in plain HTML, CSS, and JavaScript using only DOM traversal and data attributes for state — no framework, no external state library.

Requirements:
- A cart icon button showing a numeric badge, anchored via position: relative on its wrapper, with an absolutely positioned dropdown panel that toggles between display: none and display: flex via a single "open" class.
- Each cart line item stores its unit price as a data-price attribute (a numeric string) directly on the row element, and its current quantity as the text content of a small span — no separate JavaScript array or object should hold the cart state; the DOM itself is the source of truth.
- A quantity stepper with plus and minus buttons per row that read the current quantity from the row's own span, clamp the result so it can never go below 1 using Math.max, and write the new value back into that span's text.
- A getTotal function that queries every cart-item row in the document, reads its data-price attribute with parseFloat and its quantity span with parseInt, multiplies them, and sums across all rows — recomputed fresh on every call rather than cached.
- After every quantity change or item removal, call a single updateSummary function that recalculates the total item count for the header badge, the "(N items)" label, and the subtotal/total currency display (formatted with toFixed(2)) — so every mutation path (plus, minus, remove) funnels through the same recalculation function rather than each having its own update logic.
- A remove button per row that walks up to the closest cart-item row with the DOM's closest() method and removes the entire row element, then triggers the same summary recalculation.
- Include a free-shipping threshold message that reruns off the same getTotal computation, and a checkout button and a continue-shopping button that closes the dropdown.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Click the cart button to open the dropdownThe dark cart icon button in the top-right shows the item count badge. Click it to open the dropdown. Click it again or the × button to close.
  2. 2
    Adjust item quantitiesClick the + or − buttons next to any item. The quantity updates and the subtotal and total recalculate immediately. Quantity cannot go below 1.
  3. 3
    Remove an itemClick the × icon at the right edge of any item row to remove it. The item count badge and totals update automatically.
  4. 4
    Add your own itemsDuplicate a .cart-item div and update the data-price attribute, gradient background colors, SVG icon, item name, and price text. JS auto-picks up the new item.
  5. 5
    Close on outside clickAdd document.addEventListener("click", e => { if (!e.target.closest(".cart-wrap")) document.getElementById("cartDropdown").classList.remove("open"); }); to close the dropdown when clicking outside.
  6. 6
    Export for your frameworkClick "JSX" for a React component with useState cart management. Click "Vue" for a Vue 3 SFC with reactive cart array.

Real-world uses

Common Use Cases

E-commerce cart flyout on product listing pages
The mini cart lets shoppers adjust quantities and review their order without leaving the listing page. Connect the cart count badge to a cart state array and update it every time an Add to Cart button is clicked. Persist the cart in localStorage and restore it on page load.
SaaS subscription plan add-on selector summary
Use the mini cart pattern for subscription add-ons — show selected plans, seats, and billing cycle with a live subtotal. The quantity stepper maps cleanly to licence seat counts or usage units per add-on.
Event ticketing quantity selector and order summary
Each cart item becomes a ticket type: General Admission, VIP, Early Bird. The qty stepper controls seat count. The checkout button proceeds to the payment form with the order summary pre-filled.
Wire to Stripe Checkout or a headless commerce API
Store cart state in a React useState or Zustand store. On checkout click, POST the cart array to a Stripe Checkout session endpoint or Commerce.js cart API. The mini cart becomes a controlled component fed from that store.
Study dropdown positioning and DOM-based state patterns
The snippet demonstrates position: absolute anchoring, click-toggle UI patterns, and data-attribute state storage — foundational patterns in vanilla JS commerce UIs that predate React. Understanding these helps debug framework cart implementations.
Internal tools item selector and procurement UI
Adapt the cart for an internal procurement request tool: employees select supplies or software licences from a catalog, adjust quantities, and submit. The checkout button becomes Submit Request and fires a POST to an internal approval API.

Got questions?

Frequently Asked Questions

Add a click listener on document: document.addEventListener("click", e => { if (!e.target.closest(".cart-wrap")) document.getElementById("cartDropdown").classList.remove("open"); }); closest(".cart-wrap") returns null if the click originated outside the cart wrapper — the if block then removes .open. Also add a keydown listener for Escape: document.addEventListener("keydown", e => { if (e.key === "Escape") document.getElementById("cartDropdown").classList.remove("open"); }); to support keyboard users who expect Escape to dismiss overlays. After dismissing, return focus to the cart button so keyboard navigation is not lost: document.querySelector(".cart-btn").focus().

Maintain a cart array: let cart = JSON.parse(localStorage.getItem("cart") || "[]"). After every add/remove/qty change, call localStorage.setItem("cart", JSON.stringify(cart)). On DOMContentLoaded, call renderCart() to rebuild .cart-items from the saved array instead of the hardcoded HTML. Each cart item object should store id, name, price, qty, and an optional imageUrl or gradient string. renderCart() iterates the array and creates .cart-item elements dynamically using innerHTML or DOM createElement, then calls updateSummary() to set the badge count and totals. If cart is empty after loading, show an empty state inside the dropdown rather than a blank items area.

Manage cart as const [cart, setCart] = useState([]). Render a MiniCart component that maps cart items to CartItem rows. Each CartItem receives price, qty, onQtyChange(delta), and onRemove props. Compute subtotal with cart.reduce((sum, item) => sum + item.price * item.qty, 0). Use a Zustand store for global cart state so the cart icon in the header and the mini cart dropdown both read from the same source. Persist to localStorage with a Zustand middleware: { persist } from "zustand/middleware", passing the cart slice with a "cart" key so it survives page reloads.

Each .cart-item stores its unit price in a data-price attribute, and the current quantity lives in the .qty element's text. getTotal() loops over every .cart-item and sums parseFloat(item.dataset.price) * parseInt(qty). updateSummary() then writes the aggregate quantity to the #cartCount badge on the cart button, the row count to #itemCount in the header, and the formatted money values to #subtotal and #total with toFixed(2). Every interaction — plus, minus, or remove — simply calls updateSummary() again, so the DOM itself is the single source of truth and there is no separate state object to keep in sync.

Pick a threshold (say 75) and extend updateSummary(): const remaining = Math.max(0, 75 - getTotal()). If remaining is 0, show "You've unlocked free shipping!"; otherwise show "Add $" + remaining.toFixed(2) + " more for free shipping". For the bar itself, add a thin track div under the cart header and set its fill width to Math.min(100, getTotal() / 75 * 100) + "%" with a CSS transition on width so it animates as quantities change. Because updateSummary() already runs on every cart mutation, the bar stays correct for free.