
Example store
The full example storefront in one install: announcement, nav with a mobile drawer, hero, proof, categories, grid, product detail, lab results, reviews, guarantees, how it works, FAQ, CTA, footer and a working cart drawer.
npx shadcn@latest add @storekit/page-storeLive and interactive. Switch the brand in the frame header to see the same source under another token file.
Installation
npx shadcn@latest add @storekit/page-storepnpm dlx shadcn@latest add @storekit/page-storeyarn dlx shadcn@latest add @storekit/page-storebunx --bun shadcn@latest add @storekit/page-storeSource
"use client"
import { useState } from "react"
import { cn } from "@/components/storekit/lib/cn"
import { AnnouncementCountdown } from "@/components/storekit/announcement-bar/announcement-countdown"
import { NavMinimal } from "@/components/storekit/nav/nav-minimal"
import { NavDrawer } from "@/components/storekit/nav/nav-drawer"
import { CartDrawer, type CartDrawerLine } from "@/components/storekit/cart/cart-drawer"
import { HeroSplit } from "@/components/storekit/hero/hero-split"
import { TrustStats } from "@/components/storekit/trust/trust-stats"
import { TrustGuarantees } from "@/components/storekit/trust/trust-guarantees"
import { CollectionCategories } from "@/components/storekit/collection/collection-categories"
import { CollectionGrid, type GridProduct } from "@/components/storekit/collection/collection-grid"
import { PdpGallery } from "@/components/storekit/pdp/pdp-gallery"
import { PdpBuybox } from "@/components/storekit/pdp/pdp-buybox"
import { PdpCoaBand } from "@/components/storekit/pdp/pdp-coa-band"
import { ReviewsSummary } from "@/components/storekit/reviews/reviews-summary"
import { ReviewsList } from "@/components/storekit/reviews/reviews-list"
import { HowItWorks } from "@/components/storekit/marketing/how-it-works"
import { FaqAccordion } from "@/components/storekit/marketing/faq-accordion"
import { CtaBand } from "@/components/storekit/marketing/cta-band"
import { FooterColumns } from "@/components/storekit/footer/footer-columns"
/* A complete storefront composed from eighteen StoreKit blocks and nothing
else: announcement, nav (desktop bar plus mobile drawer), hero, proof,
categories, grid, product detail, lab results, reviews, guarantees, how it
works, FAQ, CTA, footer and a cart drawer that every add-to-cart opens.
Swap the sample catalog for yours; the brand tokens do the rest. */
export type StoreProduct = {
id: string
title: string
/** Integer cents. */
price: number
compareAt?: number
image: string
/** Shop category, e.g. "Powders". Drives the category tiles and grid filters. */
category: string
/** Optional product form ("powder", "capsule", "drink"...). */
form?: string
}
export type StoreCartLine = { productId: string; variantId?: string; quantity: number; subscribe?: boolean }
const SAMPLE_PRODUCTS: StoreProduct[] = [
{ id: "daily-greens", title: "Daily Greens", price: 4900, compareAt: 5900, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "electrolytes", title: "Electrolytes", price: 3200, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "magnesium-night", title: "Magnesium Night", price: 2900, image: "/placeholder.webp", category: "Capsules & gummies", form: "gummy" },
{ id: "protein", title: "Protein", price: 5400, compareAt: 6200, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "focus", title: "Focus", price: 3900, image: "/placeholder.webp", category: "Capsules & gummies", form: "capsule" },
{ id: "sparkling-recovery", title: "Sparkling Recovery", price: 3600, image: "/placeholder.webp", category: "Drinks", form: "drink" },
]
export function PageStore({
brand = { name: "Morrow", mark: "/placeholder.webp" },
products = SAMPLE_PRODUCTS,
hero = {
headline: "Daily wellness, made simple.",
subcopy: "Clean ingredients, third-party tested, made in the USA. One scoop a day, delivered on your schedule.",
image: products[0]?.image ?? "/placeholder.webp",
},
categoryImages,
ctaImage,
onAddToCart,
className,
}: {
brand?: { name: string; mark: string }
products?: StoreProduct[]
hero?: { headline: string; subcopy: string; image: string }
/** Tile image per category title. Defaults to the first product in that category. */
categoryImages?: Partial<Record<string, string>>
/** Image for the closing CTA band. Defaults to the lead product. */
ctaImage?: string
onAddToCart?: (line: StoreCartLine) => void
className?: string
}) {
const lead = products[0] ?? SAMPLE_PRODUCTS[0]
const leadVariants = [
{ id: "30", label: "30 servings", price: lead.price },
{ id: "60", label: "60 servings", price: Math.round(lead.price * 1.8), compareAt: Math.round(lead.price * 2) },
{ id: "bundle", label: "3-pouch bundle", price: Math.round(lead.price * 2.55), compareAt: Math.round(lead.price * 3) },
]
const subscribeDiscount = 0.15
// The page owns the cart lines; the drawer edits them and reports back.
// Adding remounts the drawer (it is closed at that moment) with the new lines.
const [lines, setLines] = useState<CartDrawerLine[]>([])
const [cartOpen, setCartOpen] = useState(false)
const [cartRev, setCartRev] = useState(0)
// Upsells are picked when the drawer mounts so the row does not reshuffle mid-session.
const pickUpsells = (inCart: string[]) =>
products
.filter((x) => !inCart.includes(x.id))
.slice(0, 2)
.map((x) => ({ id: x.id, title: x.title, price: x.price, image: x.image }))
const [upsells, setUpsells] = useState(() => pickUpsells([]))
const cartCount = lines.reduce((n, l) => n + l.quantity, 0)
const add = (line: StoreCartLine) => {
const product = products.find((x) => x.id === line.productId)
if (product) {
const variant = product.id === lead.id ? leadVariants.find((v) => v.id === line.variantId) : undefined
const base = variant?.price ?? product.price
const price = line.subscribe ? Math.round(base * (1 - subscribeDiscount)) : base
const compareAt = line.subscribe ? base : variant?.compareAt ?? product.compareAt
const id = [product.id, line.variantId, line.subscribe ? "sub" : ""].filter(Boolean).join(":")
const label = [variant?.label, line.subscribe ? "Subscribe and save" : ""].filter(Boolean).join(" / ") || undefined
setLines((ls) =>
ls.some((l) => l.id === id)
? ls.map((l) => (l.id === id ? { ...l, quantity: l.quantity + line.quantity } : l))
: [...ls, { id, title: product.title, variant: label, price, compareAt, image: product.image, quantity: line.quantity }]
)
setUpsells(pickUpsells([...lines.map((l) => l.id.split(":")[0]), product.id]))
setCartRev((r) => r + 1)
setCartOpen(true)
}
onAddToCart?.(line)
}
const links = [
{ label: "Shop", href: "#shop" },
{ label: "Lab results", href: "#coa" },
{ label: "Reviews", href: "#reviews" },
{ label: "FAQ", href: "#faq" },
]
const grid: GridProduct[] = products.map((x, i) => ({
id: x.id,
title: x.title,
price: x.price,
compareAt: x.compareAt,
image: x.image,
category: x.category,
form: x.form,
stock: i === 3 ? "low" : "in",
isNew: i === 2,
}))
const titles = Array.from(new Set(products.map((x) => x.category))).slice(0, 3)
const categories = titles.map((t) => ({
title: t,
count: products.filter((x) => x.category === t).length,
image: categoryImages?.[t] ?? products.find((x) => x.category === t)?.image ?? lead.image,
href: "#shop",
}))
const filters = [{ label: "All" }, ...titles.map((t) => ({ label: t, category: t }))]
return (
<div data-sk-section="page" data-sk-variant="page-store" className={cn("bg-sk-paper text-sk-ink", className)}>
<AnnouncementCountdown cutoffHour={14} />
<NavMinimal className="hidden md:block" brandName={brand.name} markSrc={brand.mark} cartCount={cartCount} onCart={() => setCartOpen(true)} links={links} />
<NavDrawer className="md:hidden" brandName={brand.name} markSrc={brand.mark} cartCount={cartCount} onCart={() => setCartOpen(true)} categories={links} />
<HeroSplit
eyebrow={brand.name}
headline={hero.headline}
subcopy={hero.subcopy}
image={hero.image}
imageAlt={lead.title}
primaryCta={{ label: "Shop the range", href: "#shop" }}
secondaryCta={{ label: "See lab results", href: "#coa" }}
specs={[
{ label: "Bestseller", value: lead.title },
{ label: "Servings", value: "30 per pouch" },
{ label: "Per serving", value: `$${(lead.price / 3000).toFixed(2)}`, highlight: true },
{ label: "Tested", value: "Third party" },
]}
/>
<section aria-label="Proof" className="mx-auto max-w-6xl px-4 sm:px-8">
<TrustStats />
</section>
<section id="shop" className="mx-auto max-w-6xl scroll-mt-20 px-4 py-12 sm:px-8 sm:py-16">
<CollectionCategories categories={categories} />
</section>
<section className="mx-auto max-w-6xl px-4 pb-12 sm:px-8 sm:pb-16">
<CollectionGrid products={grid} filters={filters} onAdd={(p) => add({ productId: (p as GridProduct).id, quantity: 1 })} />
</section>
<section className="border-y border-sk-line bg-sk-surface/60">
<div className="mx-auto grid max-w-6xl gap-10 px-4 py-12 sm:px-8 sm:py-16 lg:grid-cols-[1.05fr_1fr] lg:gap-16">
<PdpGallery images={products.slice(0, 4).map((x) => ({ src: x.image, alt: x.title }))} />
<PdpBuybox
title={lead.title}
variants={leadVariants}
subscribeDiscount={subscribeDiscount}
onAddToCart={(line) => add({ productId: lead.id, variantId: line.variantId, quantity: line.quantity, subscribe: line.subscribe })}
/>
</div>
</section>
<div id="coa" className="scroll-mt-20">
<PdpCoaBand product={lead.title} lab={`${brand.name} partner lab`} />
</div>
<section id="reviews" className="mx-auto grid scroll-mt-20 max-w-6xl gap-10 px-4 py-12 sm:px-8 sm:py-16 lg:grid-cols-[1fr_1.3fr]">
<ReviewsSummary className="self-start lg:sticky lg:top-24 lg:grid-cols-1 lg:gap-8" />
<ReviewsList />
</section>
<section className="border-t border-sk-line">
<div className="mx-auto max-w-6xl px-4 py-12 sm:px-8 sm:py-16">
<TrustGuarantees />
</div>
</section>
<HowItWorks
className="sm:py-16"
steps={[
{ title: "Pick your routine", body: "Choose a product or two. Subscribe and save 15%, skip or cancel anytime.", meta: "2 min", image: lead.image },
{ title: "Mix and go", body: "One scoop in water or a smoothie. No clumps, no chalky aftertaste.", meta: "30 sec", image: products[1]?.image ?? lead.image },
{ title: "Feel the difference", body: "Most people notice steadier energy within two weeks. If not, it's on us.", meta: "30 days", image: products[2]?.image ?? lead.image },
]}
/>
<div id="faq" className="scroll-mt-20">
<FaqAccordion className="pt-0 sm:pt-0" />
</div>
<CtaBand image={ctaImage ?? lead.image} />
<FooterColumns brandName={brand.name} markSrc={brand.mark} />
<CartDrawer
key={cartRev}
open={cartOpen}
onOpenChange={setCartOpen}
items={lines}
onLinesChange={setLines}
upsells={upsells}
/>
</div>
)
}
import { PageStore } from "@/components/storekit/page/page-store"
/* Installed as app/store/page.tsx. Next.js page files may only export the
page itself, so the store lives in components/storekit/page/page-store.tsx
and this file feeds it your catalog. Prices are integer cents. */
export default function StorePage() {
return (
<PageStore
brand={{ name: "Morrow", mark: "/placeholder.webp" }}
products={[
{ id: "daily-greens", title: "Daily Greens", price: 4900, compareAt: 5900, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "electrolytes", title: "Electrolytes", price: 3200, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "magnesium-night", title: "Magnesium Night", price: 2900, image: "/placeholder.webp", category: "Capsules & gummies", form: "gummy" },
{ id: "protein", title: "Protein", price: 5400, compareAt: 6200, image: "/placeholder.webp", category: "Powders", form: "powder" },
{ id: "focus", title: "Focus", price: 3900, image: "/placeholder.webp", category: "Capsules & gummies", form: "capsule" },
{ id: "sparkling-recovery", title: "Sparkling Recovery", price: 3600, image: "/placeholder.webp", category: "Drinks", form: "drink" },
]}
hero={{
headline: "Daily wellness, made simple.",
subcopy: "Clean ingredients, third-party tested, made in the USA. One scoop a day, delivered on your schedule.",
image: "/placeholder.webp",
}}
/>
)
}
Usage
Installs the full example store at app/store/page.tsx. Replace the sample products with your catalog.






