Blocks
E-Commerce
Product cards, lists, carts, order summaries, reviews.
Product grid
Sortable product listing with wishlist overlay, sale badges, ratings and add-to-cart actions.
New arrivals
import {
Badge,
Button,
Card,
Heading,
IconButton,
Rating,
Select,
Text,
} from "@zephora/react";
const heartIcon = (
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M12 21s-6.7-4.35-9.33-8.11C.9 10.36 1.96 6.5 5.14 5.42A5 5 0 0 1 12 7.35a5 5 0 0 1 6.86-1.93c3.18 1.08 4.24 4.94 2.47 7.47C18.7 16.65 12 21 12 21z" />
</svg>
);
interface Product {
name: string;
price: string;
oldPrice?: string;
rating: number;
reviews: number;
sale?: string;
gradient: string;
}
const products: Product[] = [
{ name: "Aero Runner 2", price: "$129.00", oldPrice: "$159.00", rating: 4, reviews: 142, sale: "-19%", gradient: "linear-gradient(135deg, #6366f1, #a855f7)" },
{ name: "Trail Blazer GTX", price: "$149.00", rating: 5, reviews: 87, gradient: "linear-gradient(135deg, #14b8a6, #0ea5e9)" },
{ name: "Street Court Low", price: "$89.00", oldPrice: "$119.00", rating: 4, reviews: 203, sale: "-25%", gradient: "linear-gradient(135deg, #f59e0b, #f43f5e)" },
{ name: "Cloud Walker Knit", price: "$109.00", rating: 4, reviews: 64, gradient: "linear-gradient(135deg, #22c55e, #14b8a6)" },
];
export function ProductGrid() {
return (
<section>
<div style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 16, flexWrap: "wrap", marginBottom: 20 }}>
<Heading level={2} size="xl">New arrivals</Heading>
<div style={{ width: 190 }}>
<Select
aria-label="Sort products"
defaultValue="featured"
options={[
{ label: "Featured", value: "featured" },
{ label: "Price: low to high", value: "price-asc" },
{ label: "Price: high to low", value: "price-desc" },
{ label: "Newest", value: "newest" },
]}
/>
</div>
</div>
<div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fill, minmax(190px, 1fr))", gap: 16 }}>
{products.map((product) => (
<Card key={product.name} padding="none" style={{ overflow: "hidden" }}>
<div style={{ position: "relative", height: 140, background: product.gradient }}>
{product.sale && (
<Badge color="danger" size="sm" style={{ position: "absolute", top: 10, left: 10 }}>
{product.sale}
</Badge>
)}
<IconButton
icon={heartIcon}
aria-label={"Add " + product.name + " to wishlist"}
size="sm"
shape="circle"
variant="soft"
style={{ position: "absolute", top: 8, right: 8 }}
/>
</div>
<div style={{ display: "grid", gap: 8, padding: 14 }}>
<Text weight="semibold" truncate>{product.name}</Text>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<Rating value={product.rating} readOnly size="sm" />
<Text size="sm" muted>({product.reviews})</Text>
</div>
<div style={{ display: "flex", alignItems: "baseline", gap: 8 }}>
{product.oldPrice && (
<Text size="sm" muted style={{ textDecoration: "line-through" }}>
{product.oldPrice}
</Text>
)}
<Text weight="bold">{product.price}</Text>
</div>
<Button size="sm" fullWidth>Add to cart</Button>
</div>
</Card>
))}
</div>
</section>
);
}
Product detail
Two-column product page with image carousel, color picker, quantity stepper and info accordion.
Aero Runner 2
Engineered mesh upper with a responsive foam midsole and a carbon-infused plate for race-day energy return. Weight: 238 g in size 42.
Free standard shipping on orders over $75. Express delivery available at checkout.
30-day free returns. Items must be unworn and in the original packaging.
import {
Accordion,
AccordionItem,
Badge,
Breadcrumb,
Button,
Carousel,
Heading,
InputNumber,
Rating,
Text,
ToggleGroup,
ToggleGroupItem,
} from "@zephora/react";
const heartIcon = (
<svg viewBox="0 0 24 24" width="16" height="16" fill="currentColor" aria-hidden="true">
<path d="M12 21s-6.7-4.35-9.33-8.11C.9 10.36 1.96 6.5 5.14 5.42A5 5 0 0 1 12 7.35a5 5 0 0 1 6.86-1.93c3.18 1.08 4.24 4.94 2.47 7.47C18.7 16.65 12 21 12 21z" />
</svg>
);
const slides = [
"linear-gradient(135deg, #6366f1, #a855f7)",
"linear-gradient(135deg, #14b8a6, #0ea5e9)",
"linear-gradient(135deg, #f59e0b, #f43f5e)",
];
const colors = [
{ value: "indigo", label: "Indigo", swatch: "#6366f1" },
{ value: "teal", label: "Teal", swatch: "#14b8a6" },
{ value: "rose", label: "Rose", swatch: "#f43f5e" },
];
export function ProductDetail() {
return (
<div style={{ display: "flex", gap: 32, flexWrap: "wrap" }}>
<div style={{ flex: "1 1 320px", minWidth: 0 }}>
<Carousel aria-label="Product images">
{slides.map((gradient) => (
<div key={gradient} style={{ height: 300, borderRadius: 12, background: gradient }} />
))}
</Carousel>
</div>
<div style={{ flex: "1 1 320px", minWidth: 0, display: "grid", gap: 16, alignContent: "start" }}>
<Breadcrumb
items={[
{ label: "Home", href: "#home" },
{ label: "Sneakers", href: "#sneakers" },
{ label: "Aero Runner 2" },
]}
/>
<Heading level={2} size="2xl">Aero Runner 2</Heading>
<div style={{ display: "flex", alignItems: "center", gap: 8 }}>
<Rating value={4} readOnly size="sm" />
<a href="#reviews" style={{ fontSize: 14 }}>142 reviews</a>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Text size="xl" weight="bold">$129.00</Text>
<Badge variant="soft" color="success" size="sm">In stock</Badge>
</div>
<div style={{ display: "grid", gap: 6 }}>
<Text size="sm" weight="medium">Color</Text>
<ToggleGroup type="single" defaultValue="indigo" size="sm">
{colors.map((color) => (
<ToggleGroupItem key={color.value} value={color.value}>
<span
style={{ width: 10, height: 10, borderRadius: "50%", background: color.swatch, display: "inline-block", marginRight: 6 }}
aria-hidden="true"
/>
{color.label}
</ToggleGroupItem>
))}
</ToggleGroup>
</div>
<div style={{ display: "grid", gap: 6, width: 130 }}>
<Text size="sm" weight="medium">Quantity</Text>
<InputNumber aria-label="Quantity" defaultValue={1} min={1} max={10} />
</div>
<div style={{ display: "flex", gap: 12, flexWrap: "wrap" }}>
<Button size="lg">Add to cart</Button>
<Button size="lg" variant="outline" startIcon={heartIcon}>Wishlist</Button>
</div>
<Accordion type="single" collapsible defaultValue="details">
<AccordionItem value="details" title="Product details">
<Text as="p" size="sm" muted>
Engineered mesh upper with a responsive foam midsole and a carbon-infused plate for
race-day energy return. Weight: 238 g in size 42.
</Text>
</AccordionItem>
<AccordionItem value="shipping" title="Shipping">
<Text as="p" size="sm" muted>
Free standard shipping on orders over $75. Express delivery available at checkout.
</Text>
</AccordionItem>
<AccordionItem value="returns" title="Returns">
<Text as="p" size="sm" muted>
30-day free returns. Items must be unworn and in the original packaging.
</Text>
</AccordionItem>
</Accordion>
</div>
</div>
);
}
Cart panel
Shopping cart with editable quantities, item removal, totals breakdown and checkout action.
Your cart
3 itemsimport {
Badge,
Button,
Card,
Chip,
Divider,
Heading,
IconButton,
InputNumber,
Text,
} from "@zephora/react";
const trashIcon = (
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" aria-hidden="true">
<path d="M4 7h16M10 11v6M14 11v6M6 7l1 12a2 2 0 0 0 2 2h6a2 2 0 0 0 2-2l1-12M9 7V5a2 2 0 0 1 2-2h2a2 2 0 0 1 2 2v2" />
</svg>
);
const items = [
{ name: "Aero Runner 2", variant: "Size 42 · Indigo", price: "$129.00", gradient: "linear-gradient(135deg, #6366f1, #a855f7)" },
{ name: "Trail Blazer GTX", variant: "Size 41 · Teal", price: "$149.00", gradient: "linear-gradient(135deg, #14b8a6, #0ea5e9)" },
{ name: "Court Sock 2-pack", variant: "One size · White", price: "$18.00", gradient: "linear-gradient(135deg, #f59e0b, #f43f5e)" },
];
export function CartPanel() {
return (
<Card padding="lg" style={{ maxWidth: 520, margin: "0 auto" }}>
<div style={{ display: "flex", alignItems: "center", gap: 10, marginBottom: 16 }}>
<Heading level={3} size="lg">Your cart</Heading>
<Badge variant="soft" color="neutral" size="sm">3 items</Badge>
</div>
<div style={{ display: "grid", gap: 16 }}>
{items.map((item) => (
<div key={item.name} style={{ display: "flex", alignItems: "center", gap: 12 }}>
<div style={{ width: 56, height: 56, borderRadius: 8, background: item.gradient, flexShrink: 0 }} />
<div style={{ flex: 1, minWidth: 0, display: "grid", gap: 4, justifyItems: "start" }}>
<Text weight="semibold" size="sm" truncate>{item.name}</Text>
<Chip size="sm">{item.variant}</Chip>
</div>
<div style={{ width: 92, flexShrink: 0 }}>
<InputNumber size="sm" defaultValue={1} min={1} aria-label={"Quantity for " + item.name} />
</div>
<Text weight="semibold" size="sm" style={{ width: 64, textAlign: "right" }} as="div">
{item.price}
</Text>
<IconButton icon={trashIcon} aria-label={"Remove " + item.name} variant="ghost" size="sm" />
</div>
))}
</div>
<Divider spacing={5} />
<div style={{ display: "grid", gap: 8 }}>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Text size="sm" muted>Subtotal</Text>
<Text size="sm">$296.00</Text>
</div>
<div style={{ display: "flex", justifyContent: "space-between" }}>
<Text size="sm" muted>Shipping</Text>
<Text size="sm">Free</Text>
</div>
<div style={{ display: "flex", justifyContent: "space-between", marginTop: 4 }}>
<Text weight="bold">Total</Text>
<Text weight="bold">$296.00</Text>
</div>
</div>
<Button fullWidth size="lg" style={{ marginTop: 20 }}>Checkout</Button>
</Card>
);
}
Order summary
Order confirmation with delivery progress stepper, bordered details list, line items and total band.
- OrderedMay 12
- ShippedMay 14
- DeliveredEst. May 17
Order summary
- Order number
- #ZP-10482
- Order date
- May 12, 2026
- Payment
- Visa ending 4242
- Shipping method
- Standard (free)
- Delivery address
- Ada Lovelace, 42 Analytical St, London N1 9GU
| Item | Qty | Price |
|---|---|---|
| Aero Runner 2 | 1 | $129.00 |
| Trail Blazer GTX | 1 | $149.00 |
| Court Sock 2-pack | 2 | $36.00 |
import { Card, Descriptions, Heading, Stepper, Text } from "@zephora/react";
const lineItems = [
{ name: "Aero Runner 2", qty: 1, price: "$129.00" },
{ name: "Trail Blazer GTX", qty: 1, price: "$149.00" },
{ name: "Court Sock 2-pack", qty: 2, price: "$36.00" },
];
const cellBorder = "1px solid rgba(128, 128, 128, 0.25)";
export function OrderSummary() {
return (
<Card padding="lg" style={{ maxWidth: 720, margin: "0 auto" }}>
<Stepper
activeStep={1}
steps={[
{ title: "Ordered", description: "May 12" },
{ title: "Shipped", description: "May 14" },
{ title: "Delivered", description: "Est. May 17" },
]}
style={{ marginBottom: 24 }}
/>
<Heading level={3} size="lg" style={{ marginBottom: 12 }}>Order summary</Heading>
<Descriptions
bordered
columns={2}
size="sm"
items={[
{ label: "Order number", content: "#ZP-10482" },
{ label: "Order date", content: "May 12, 2026" },
{ label: "Payment", content: "Visa ending 4242" },
{ label: "Shipping method", content: "Standard (free)" },
{ label: "Delivery address", content: "Ada Lovelace, 42 Analytical St, London N1 9GU", span: 2 },
]}
/>
<table style={{ width: "100%", borderCollapse: "collapse", marginTop: 20, fontSize: 14 }}>
<thead>
<tr>
<th style={{ textAlign: "left", padding: "8px 0", borderBottom: cellBorder, fontWeight: 500 }}>Item</th>
<th style={{ textAlign: "center", padding: "8px 0", borderBottom: cellBorder, fontWeight: 500, width: 60 }}>Qty</th>
<th style={{ textAlign: "right", padding: "8px 0", borderBottom: cellBorder, fontWeight: 500, width: 90 }}>Price</th>
</tr>
</thead>
<tbody>
{lineItems.map((item) => (
<tr key={item.name}>
<td style={{ padding: "10px 0", borderBottom: cellBorder }}>{item.name}</td>
<td style={{ padding: "10px 0", borderBottom: cellBorder, textAlign: "center" }}>{item.qty}</td>
<td style={{ padding: "10px 0", borderBottom: cellBorder, textAlign: "right" }}>{item.price}</td>
</tr>
))}
</tbody>
</table>
<div
style={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
marginTop: 16,
padding: "12px 16px",
borderRadius: 10,
background: "rgba(99, 102, 241, 0.08)",
}}
>
<Text weight="semibold">Total (incl. tax)</Text>
<Text weight="bold" size="lg">$314.00</Text>
</div>
</Card>
);
}
Review section
Rating summary with star distribution bars next to a list of customer reviews with helpful actions.
4.6
Based on 248 reviewsExtremely comfortable straight out of the box. I ran a half marathon in these after one week and had zero blisters. The knit upper breathes well even in summer heat.
Great cushioning and a stable ride. Sizing runs about half a size small, so order up. Removed one star because the laces are a bit short for a runner knot.
import { Avatar, Chip, Divider, Heading, Progress, Rating, Text } from "@zephora/react";
const thumbIcon = (
<svg viewBox="0 0 24 24" width="14" height="14" fill="currentColor" aria-hidden="true">
<path d="M2 10h4v11H2zM22 11c0-1.1-.9-2-2-2h-5.3l.95-4.57A1.5 1.5 0 0 0 14.2 2.6L8 9.2V21h10a2 2 0 0 0 2-1.6l1.9-7A2 2 0 0 0 22 11z" />
</svg>
);
const distribution = [
{ stars: 5, percent: 72 },
{ stars: 4, percent: 18 },
{ stars: 3, percent: 6 },
{ stars: 2, percent: 3 },
{ stars: 1, percent: 1 },
];
const reviews = [
{
name: "Maya Chen",
date: "May 2, 2026",
rating: 5,
text: "Extremely comfortable straight out of the box. I ran a half marathon in these after one week and had zero blisters. The knit upper breathes well even in summer heat.",
helpful: 12,
},
{
name: "Jonas Weber",
date: "Apr 18, 2026",
rating: 4,
text: "Great cushioning and a stable ride. Sizing runs about half a size small, so order up. Removed one star because the laces are a bit short for a runner knot.",
helpful: 5,
},
];
export function ReviewSection() {
return (
<div style={{ display: "flex", gap: 40, flexWrap: "wrap" }}>
<div style={{ flex: "0 1 240px", display: "grid", gap: 12, alignContent: "start" }}>
<Heading level={3} size="4xl">4.6</Heading>
<Rating value={4.6} readOnly size="sm" />
<Text size="sm" muted>Based on 248 reviews</Text>
<div style={{ display: "grid", gap: 8, marginTop: 8 }}>
{distribution.map((row) => (
<div key={row.stars} style={{ display: "flex", alignItems: "center", gap: 10 }}>
<Text as="div" size="sm" muted style={{ width: 14, textAlign: "right" }}>
{row.stars}
</Text>
<div style={{ flex: 1 }}>
<Progress value={row.percent} size="sm" aria-label={row.stars + " star share"} />
</div>
<Text as="div" size="sm" muted style={{ width: 36 }}>{row.percent}%</Text>
</div>
))}
</div>
</div>
<div style={{ flex: "1 1 360px", minWidth: 0 }}>
{reviews.map((review, index) => (
<div key={review.name}>
{index > 0 && <Divider spacing={5} />}
<div style={{ display: "grid", gap: 10, justifyItems: "start" }}>
<div style={{ display: "flex", alignItems: "center", gap: 12 }}>
<Avatar name={review.name} size="sm" />
<div>
<Text weight="semibold" size="sm" as="div">{review.name}</Text>
<Text size="xs" muted as="div">{review.date}</Text>
</div>
</div>
<Rating value={review.rating} readOnly size="sm" />
<Text as="p" size="sm" muted>{review.text}</Text>
<div style={{ display: "flex", gap: 8 }}>
<Chip size="sm" icon={thumbIcon} onClick={() => undefined}>
Helpful ({review.helpful})
</Chip>
<Chip size="sm" onClick={() => undefined}>Report</Chip>
</div>
</div>
</div>
))}
</div>
</div>
);
}
Promo banner
Full-width gradient sale band with limited-offer badge, countdown keys and a shop-now call to action.
Summer sale ends soon
import { Badge, Button, Heading, Kbd, Text } from "@zephora/react";
export function PromoBanner() {
return (
<section
style={{
background: "linear-gradient(120deg, #4f46e5, #9333ea, #db2777)",
borderRadius: 16,
padding: "36px 40px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
gap: 32,
flexWrap: "wrap",
color: "#fff",
}}
>
<div style={{ display: "grid", gap: 12, justifyItems: "start" }}>
<Badge color="warning" size="sm">Limited offer</Badge>
<Heading level={2} size="2xl" style={{ color: "#fff" }}>
Summer sale ends soon
</Heading>
<div style={{ display: "flex", alignItems: "center", gap: 6 }}>
<Kbd size="lg">12</Kbd>
<span aria-hidden="true">:</span>
<Kbd size="lg">04</Kbd>
<span aria-hidden="true">:</span>
<Kbd size="lg">33</Kbd>
<Text size="sm" style={{ color: "rgba(255, 255, 255, 0.75)", marginLeft: 8 }}>
left to save
</Text>
</div>
</div>
<div style={{ display: "flex", alignItems: "center", gap: 24, flexWrap: "wrap" }}>
<Text as="div" weight="bold" style={{ fontSize: 56, lineHeight: 1, color: "#fff" }}>
-40%
</Text>
<Button size="lg" style={{ background: "#fff", color: "#4f46e5" }}>Shop now</Button>
</div>
</section>
);
}
Category cards
Horizontally scrollable row of gradient category cards with item counts and browse arrows.
import { Card, Heading, IconButton, ScrollArea, Text } from "@zephora/react";
const arrowIcon = (
<svg viewBox="0 0 24 24" width="16" height="16" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M5 12h14M13 6l6 6-6 6" />
</svg>
);
const categories = [
{ name: "Running", count: "128 items", gradient: "linear-gradient(135deg, #6366f1, #a855f7)" },
{ name: "Outdoor", count: "94 items", gradient: "linear-gradient(135deg, #14b8a6, #0ea5e9)" },
{ name: "Lifestyle", count: "212 items", gradient: "linear-gradient(135deg, #f59e0b, #f43f5e)" },
];
export function CategoryCards() {
return (
<ScrollArea orientation="horizontal" aria-label="Shop by category">
<div style={{ display: "flex", gap: 16, paddingBottom: 8 }}>
{categories.map((category) => (
<Card key={category.name} padding="none" style={{ flex: "0 0 280px", overflow: "hidden" }}>
<div
style={{
position: "relative",
height: 160,
background: category.gradient,
display: "flex",
flexDirection: "column",
justifyContent: "flex-end",
padding: 20,
}}
>
<IconButton
icon={arrowIcon}
aria-label={"Browse " + category.name}
shape="circle"
size="sm"
variant="soft"
style={{ position: "absolute", top: 12, right: 12 }}
/>
<Heading level={3} size="lg" style={{ color: "#fff" }}>{category.name}</Heading>
<Text size="sm" style={{ color: "rgba(255, 255, 255, 0.85)" }}>{category.count}</Text>
</div>
</Card>
))}
</div>
</ScrollArea>
);
}