Empty cart
Token-drawn bag illustration, a human line of copy, a bestsellers action and a two-item rail.
componentemptyanycartatcrecommendations
$
npx shadcn@latest add @storekit/cart-emptycart-empty
Live and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
$
npx shadcn@latest add @storekit/cart-empty$
pnpm dlx shadcn@latest add @storekit/cart-empty$
yarn dlx shadcn@latest add @storekit/cart-empty$
bunx --bun shadcn@latest add @storekit/cart-emptySource
components/storekit/cart/cart-empty.tsx
119 lines
"use client"
import * as React from "react"
import { useState } from "react"
import { motion, useReducedMotion } from "motion/react"
import { Check, Plus } from "lucide-react"
import { cn } from "@/components/storekit/lib/cn"
import { Button } from "@/components/storekit/ui/button"
import { Price } from "@/components/storekit/ui/price"
import { ProductImage } from "@/components/storekit/ui/product-image"
import { useSkBlock } from "@/components/storekit/lib/sk-analytics"
/* Empty cart: a quiet token-drawn illustration, a human line of copy, one
clear action and a two-item rail so the next step is one tap away. */
const EASE: [number, number, number, number] = [0.16, 1, 0.3, 1]
type Suggestion = { id: string; title: string; price: number; compareAt?: number; image: string }
const SAMPLE: Suggestion[] = [
{ id: "daily-greens", title: "Daily Greens", price: 4900, image: "/brands/morrow/greens.webp" },
{ id: "electrolytes", title: "Electrolytes", price: 3200, image: "/brands/morrow/electrolytes.webp" },
]
function BagIllustration() {
return (
<svg viewBox="0 0 120 120" className="h-28 w-28" aria-hidden>
<circle cx="60" cy="60" r="54" className="fill-sk-surface stroke-sk-accent/40" strokeWidth="1.5" strokeDasharray="4 6" />
<path d="M38 46h44l-3 40a6 6 0 0 1-6 5.5H47a6 6 0 0 1-6-5.5L38 46z" className="fill-sk-card stroke-sk-ink/50" strokeWidth="2" strokeLinejoin="round" />
<path d="M49 52v-9a11 11 0 0 1 22 0v9" className="fill-none stroke-sk-ink/50" strokeWidth="2" strokeLinecap="round" />
<path d="M52 70h16" className="stroke-sk-line" strokeWidth="2" strokeLinecap="round" />
</svg>
)
}
export function CartEmpty({
suggestions = SAMPLE,
onShop,
onAdd,
className,
}: {
suggestions?: Suggestion[]
onShop?: () => void
onAdd?: (id: string) => void
className?: string
}) {
const reduce = useReducedMotion()
const sk = useSkBlock("cart", "cart-empty")
const [added, setAdded] = useState<string | null>(null)
const add = (id: string) => {
setAdded(id)
sk.emit("interact", "suggestion_add")
onAdd?.(id)
window.setTimeout(() => setAdded((a) => (a === id ? null : a)), 1200)
}
const rise = (i: number) => ({
initial: reduce ? false : { opacity: 0, y: 8 },
animate: { opacity: 1, y: 0 },
transition: { duration: 0.5, delay: i * 0.05, ease: EASE },
})
return (
<div ref={sk.ref as React.Ref<HTMLDivElement>} {...sk.attrs} className={cn("mx-auto flex w-full max-w-sm flex-col items-center text-center", className)}>
<motion.div {...rise(0)}>
<BagIllustration />
</motion.div>
<motion.h3 {...rise(1)} className="mt-5 font-sk-display text-xl font-semibold leading-[1.1] tracking-[-0.02em]">
Your cart is empty.
</motion.h3>
<motion.p {...rise(2)} className="mt-2 text-sm text-sk-muted">
The best sellers are a good place to start.
</motion.p>
<motion.div {...rise(3)} className="mt-5">
<Button
onClick={() => {
sk.emit("convert", "shop_cta")
onShop?.()
}}
>
Shop bestsellers
</Button>
</motion.div>
{suggestions.length ? (
<motion.div {...rise(4)} className="mt-10 w-full text-left">
<p className="font-sk-mono text-[11px] uppercase tracking-[0.14em] text-sk-muted">Most reordered</p>
<ul className="mt-3 grid grid-cols-2 gap-3">
{suggestions.slice(0, 2).map((s) => {
const done = added === s.id
return (
<li key={s.id} className="flex flex-col overflow-hidden rounded-sk-card border border-sk-line bg-sk-card">
<ProductImage src={s.image} alt={s.title} fit="cover" />
<div className="flex flex-1 flex-col gap-2 p-3">
<p className="line-clamp-2 hyphens-auto break-words text-sm font-medium leading-snug">{s.title}</p>
<div className="mt-auto flex items-end justify-between gap-2">
<Price amount={s.price} compareAt={s.compareAt} size="sm" className="min-w-0" />
<button
type="button"
aria-label={done ? `${s.title} added` : `Add ${s.title}`}
onClick={() => add(s.id)}
className={cn(
"grid h-11 w-11 shrink-0 place-items-center rounded-sk-btn border transition-[transform,background-color,color] duration-200 active:scale-[0.97]",
"focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-sk-accent focus-visible:ring-offset-2 focus-visible:ring-offset-sk-paper",
done ? "border-sk-savings/40 text-sk-savings" : "border-sk-line text-sk-ink hover:bg-sk-ink/[0.05]"
)}
>
{done ? <Check size={16} /> : <Plus size={16} />}
</button>
</div>
</div>
</li>
)
})}
</ul>
</motion.div>
) : null}
</div>
)
}
Usage
Import from "@/components/storekit/cart/cart-empty" and pass your product data. The demo above exercises the full prop surface with sample values.

