bigbullui logobigbullui
Componentfeedback

Cart Drawer

Slide-over cart drawer with quantity controls and animated removal.

Preview

Live Preview
Premium Headphones
25%

Premium Headphones

$299$399

Jordan Daisy

Product Designer

AvailableRemote
The Future of Design Systems

The Future of Design Systems

Alex Rivera8 min read
Oc
t 14

Tech Conference 2026

21:00
Berlin Arena
Invoice #INV-2026-0987draft
Date: 2026-09-15Due: 2026-10-15
ItemQtyPrice
Design Services1$150
Development Hours5$100
SubtotalNaN
Tax (10%)NaN
TotalNaN
THERMAL RECEIPT#PTBH1E
Design Consultation$150
Development Hours$200
Total$385.00
Thank you for your purchase!
Premium Headphones
1
Phone Case
2
Subtotal$377.00
Tax$30.16
Total$407.16
Color
Size
Order #102938Processing
Design Services1x
Development5x
VIP

LOYALTY TIER

$100

Jordan Daisy

Happy Birthday!

J

Jordan Daisy

Product Designer

UX

Badge: EMP-8942

Wedding Reception

Please confirm your attendance

Installation

Tokens guide →

Add to your project via CLI (zero dependencies, code you own):

npx bigbullui add cart-drawer

Or install the full npm package:

npm install bigbullui

Usage

import { CartDrawer } from "@/components/ui/cart-drawer";

<CartDrawer />

Source code

cart-drawer.tsx

Zero dependencies (only React + utils). Copy and paste directly into your project:

"use client";

import * as React from "react";
import { cn } from "./lib/utils";

export interface CartDrawerProps {
  open: boolean;
  onOpenChange: (open: boolean) => void;
  items: { id: string; title: string; price: string; qty: number }[];
  onRemove?: (id: string) => void;
  onQtyChange?: (id: string, qty: number) => void;
  className?: string;
}

const SlideIn = "cart-drawer-slide-in";
const Fade = "cart-item-fade";
const FocusTrap = "focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring";

export function CartDrawer({
  open,
  onOpenChange,
  items,
  onRemove,
  onQtyChange,
  className,
}: CartDrawerProps) {
  const [focusedEl, setFocusedEl] = React.useState<HTMLDivElement | null>(null);

  React.useEffect(() => {
    if (!open) return;
    const handleKeyDown = (e: KeyboardEvent) => {
      if (e.key === "Escape") {
        onOpenChange(false);
      }
    };
    document.addEventListener("keydown", handleKeyDown);
    return () => document.removeEventListener("keydown", handleKeyDown);
  }, [open, onOpenChange]);

  return (
    <div
      className={cn(
        "fixed inset-0 z-50 hidden items-center justify-center motion-reduce:hidden",
        open && "block",
        open && SlideIn
      )}
      onClick={(e: React.MouseEvent) => {
        if (e.target === e.currentTarget) {
          onOpenChange(false);
        }
      }}
    >
      {/* Overlay */}
      {open && (
        <div
          className={cn(
            "absolute inset-0 bg-black/40 backdrop-blur-sm",
            FocusTrap
          )}
          onClick={(e: React.MouseEvent) => {
            if (e.target === e.currentTarget) {
              onOpenChange(false);
            }
          }}
        />
      )}

      {/* Drawer container */}
      {open && (
        <div
          className={cn(
            "relative w-80 max-w-full bg-card rounded-lg border border-foreground shadow-lg p-6 transform transition-transform duration-300 motion-reduce:transition-none",
            open && "translate-x-0",
            open && SlideIn,
            "shadow-md"
          )}
        >
          {/* Header */}
          <div className="flex items-center justify-between border-b border-border/80 pb-4 mb-4">
            <h3 className="font-mono text-sm font-bold uppercase tracking-widest">Cart</h3>
            <button
              onClick={() => onOpenChange(false)}
              className={cn(
                "p-1 rounded-md hover:bg-secondary/20 transition-colors",
                FocusTrap
              )}
            >
              ✕
            </button>
          </div>

          {/* Empty state */}
          {items.length === 0 && (
            <p className="text-sm text-muted-foreground">Your cart is empty</p>
          )}

 {/* Items */}
          {items.map((it, i) => (
            <div
              key={i}
              className={cn(
                "flex items-baseline py-2 border-b border-border/50 last:border-0",
                Fade
              )}
            >
              <span className="font-medium line-clamp-1 flex-1">{it.title}</span>
              <div className="flex items-baseline gap-2">
                <button
                  onClick={() => onQtyChange?.(it.id, Math.max(it.qty - 1, 0))}
                  className={cn(
                    "rounded border border-border w-6 h-6 flex items-center justify-center text-xs font-mono",
                    "motion-reduce:transition-none"
                  )}
                  aria-label="decrease qty"
                >
                  −
                </button>
                <span className="font-mono w-8 text-center">{it.qty}</span>
                <button
                  onClick={() => onQtyChange?.(it.id, it.qty + 1)}
                  className={cn(
                    "rounded border border-border w-6 h-6 flex items-center justify-center text-xs font-mono",
                    "motion-reduce:transition-none"
                  )}
                  aria-label="increase qty"
                >
                  +
                </button>
              </div>
              {onRemove && (
                <button
                  onClick={() => onRemove(it.id)}
                  className={cn(
                    "ml-3 rounded border border-destructive text-destructive text-xs hover:bg-destructive/10 transition-colors",
                    FocusTrap
                  )}
                  aria-label="remove item"
                >
                  remove
                </button>
              )}
            </div>
          ))}

          {/* Footer */}
          <div className="mt-4 pt-4 border-t border-border/50">
            <div className="flex items-baseline justify-between">
              <span className="text-sm font-mono text-muted-foreground">Subtotal</span>
              <span className="font-mono text-right">$0.00</span>
            </div>
            <button
              onClick={() => onOpenChange(false)}
              className={cn(
                "w-full rounded-md bg-accent text-accent-foreground px-4 py-2 text-sm font-semibold uppercase tracking-widest hover:bg-accent/90 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent transition-colors duration-150 motion-reduce:transition-none motion-reduce:focus-visible:ring-0",
                Fade
              )}
            >
              Checkout
            </button>
          </div>
        </div>
      )}
    </div>
  );
}

Props

PropTypeDescription
openbooleanControlled open state (required).
onOpenChange(open: boolean) => voidOpen state callback (required).
items{ id: string; title: string; price: string; qty: number }[]Cart line items (required).
onRemove(id: string) => voidRemove-item callback.
onQtyChange(id: string, qty: number) => voidQuantity-change callback.