Componentfeedback
Cart Drawer
Slide-over cart drawer with quantity controls and animated removal.
Preview
Live Preview

25%
Premium Headphones
$299$399
Jordan Daisy
Product Designer
AvailableRemote

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
| Item | Qty | Price |
|---|---|---|
| Design Services | 1 | $150 |
| Development Hours | 5 | $100 |
SubtotalNaN
Tax (10%)NaN
TotalNaN
THERMAL RECEIPT#I2WZHM
Design Consultation$150
Development Hours$200
Total$385.00
Thank you for your purchase!
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-drawerOr install the full npm package:
npm install bigbulluiUsage
import { CartDrawer } from "@/components/ui/cart-drawer";
<CartDrawer />Source code
cart-drawer.tsxZero 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
| Prop | Type | Description |
|---|---|---|
| open | boolean | Controlled open state (required). |
| onOpenChange | (open: boolean) => void | Open state callback (required). |
| items | { id: string; title: string; price: string; qty: number }[] | Cart line items (required). |
| onRemove | (id: string) => void | Remove-item callback. |
| onQtyChange | (id: string, qty: number) => void | Quantity-change callback. |