Products
Grids and the product page.
- 5 blocks
- 2 free
- 3 Pro
- Light and dark
- Responsive
Unlock 55 Pro blocks and every template
Every preview is open. With Pro you get the code, copy-paste ready, with every update for good.
Product cards
FreeFour products with a price and quick add.
product-cards.tsx
"use client";
import { Camera, Check, Cpu, Plus, ScanLine, TrafficCone } from "lucide-react";
import * as React from "react";
import { Button, toast } from "@waveinno/ui";
const PRODUCTS = [
{ id: "arm", name: "Barrier arm, 4 m", line: "Opens in 1.8 s", price: 38500, hue: 230, icon: <TrafficCone /> },
{ id: "cam", name: "Plate camera", line: "Reads at night", price: 24900, hue: 265, icon: <Camera /> },
{ id: "ctl", name: "Gate controller", line: "Works offline", price: 15500, hue: 285, icon: <Cpu /> },
{ id: "scan", name: "Handheld scanner", line: "For attendants", price: 12400, hue: 195, icon: <ScanLine /> },
];
export default function ProductCards() {
const [added, setAdded] = React.useState<string[]>([]);
return (
<section className="px-4 py-16 sm:px-6">
<div className="mx-auto max-w-6xl">
<div className="flex items-end justify-between">
<h2 className="disp text-[26px] font-bold leading-9 tracking-[-0.025em] text-text">Popular hardware</h2>
<a href="#" className="text-[13.5px] font-semibold text-primary hover:underline">
See all
</a>
</div>
<ul className="mt-6 grid grid-cols-2 gap-4 lg:grid-cols-4">
{PRODUCTS.map((p) => {
const isIn = added.includes(p.id);
return (
<li key={p.id} className="group">
<div
className="grid aspect-square place-items-center overflow-hidden rounded-[18px] border border-border text-[color:var(--tile)] transition-transform duration-500 group-hover:scale-[1.02] [&_svg]:size-14 [&_svg]:stroke-[1.25]"
style={{ "--tile": `hsl(${p.hue} 50% 40%)`, background: `radial-gradient(80% 70% at 30% 20%, hsl(${p.hue} 90% 72% / .5), transparent 70%), linear-gradient(160deg, hsl(${p.hue} 60% 96%), hsl(${p.hue + 30} 40% 88%))` } as React.CSSProperties}
>
{p.icon}
</div>
<div className="mt-3 flex items-start justify-between gap-2">
<div>
<h3 className="text-[14.5px] font-semibold text-text">{p.name}</h3>
<p className="text-[12.5px] text-muted">{p.line}</p>
</div>
<p className="num text-[14.5px] font-semibold text-text">৳{p.price.toLocaleString("en-IN")}</p>
</div>
<Button
variant={isIn ? "secondary" : "primary"}
size="sm"
className="mt-3 w-full"
onClick={() => {
setAdded((a) => (isIn ? a.filter((x) => x !== p.id) : [...a, p.id]));
if (!isIn) toast.success(`${p.name} added to the cart`);
}}
>
{isIn ? <Check /> : <Plus />} {isIn ? "In the cart" : "Add to cart"}
</Button>
</li>
);
})}
</ul>
</div>
</section>
);
}Product list
FreeRows with specs, stock and price.
product-list.tsx
import { Camera, Cpu, Monitor, ScanLine, TrafficCone } from "lucide-react";
import { Badge, Button, Rating } from "@waveinno/ui";
const PRODUCTS = [
{ name: "Barrier arm, 4 m", specs: ["Opens in 1.8 s", "IP54", "3 million cycles"], price: 38500, stock: 14, rating: 4.8, hue: 230, icon: <TrafficCone /> },
{ name: "Plate camera, night", specs: ["1080p", "Infrared to 30 m", "PoE"], price: 24900, stock: 3, rating: 4.9, hue: 265, icon: <Camera /> },
{ name: "Gate controller", specs: ["Works offline", "4 gates", "Battery backup"], price: 15500, stock: 22, rating: 4.9, hue: 285, icon: <Cpu /> },
{ name: "LED space counter", specs: ["Outdoor", "3 digits", "Arrow display"], price: 18900, stock: 0, rating: 4.7, hue: 340, icon: <Monitor /> },
{ name: "Handheld scanner", specs: ["Android", "QR and plates", "8 h battery"], price: 12400, stock: 9, rating: 4.4, hue: 195, icon: <ScanLine /> },
];
export default function ProductList() {
return (
<section className="px-4 py-14 sm:px-6">
<div className="mx-auto max-w-4xl">
<h2 className="disp text-[24px] font-bold leading-8 text-text">All hardware</h2>
<ul className="mt-6 divide-y divide-border rounded-[20px] border border-border bg-raised shadow-[var(--lit)]">
{PRODUCTS.map((p) => (
<li key={p.name} className="flex flex-wrap items-center gap-4 p-4 sm:flex-nowrap">
<span
className="grid size-20 shrink-0 place-items-center rounded-[14px] border border-border [&_svg]:size-8 [&_svg]:stroke-[1.4]"
style={{ color: `hsl(${p.hue} 50% 40%)`, background: `linear-gradient(160deg, hsl(${p.hue} 60% 96%), hsl(${p.hue + 30} 40% 86%))` }}
>
{p.icon}
</span>
<div className="min-w-0 flex-1">
<div className="flex items-center gap-2">
<h3 className="text-[15px] font-semibold text-text">{p.name}</h3>
<Rating readOnly value={p.rating} allowHalf size="sm" />
</div>
<ul className="mt-1.5 flex flex-wrap gap-1.5">
{p.specs.map((s) => (
<li key={s} className="rounded-full bg-hover px-2 py-0.5 text-[11.5px] text-body">
{s}
</li>
))}
</ul>
</div>
<div className="flex w-full items-center justify-between gap-4 sm:w-auto sm:flex-col sm:items-end sm:gap-1.5">
<p className="num text-[16px] font-bold text-text">৳{p.price.toLocaleString("en-IN")}</p>
{p.stock === 0 ? <Badge tone="danger">Out of stock</Badge> : p.stock < 5 ? <Badge tone="warning">{p.stock} left</Badge> : <Badge tone="success">In stock</Badge>}
</div>
<Button variant="secondary" size="sm" disabled={p.stock === 0} className="w-full sm:w-auto">
{p.stock === 0 ? "Notify me" : "Add"}
</Button>
</li>
))}
</ul>
</div>
</section>
);
}Product grid
ProFilters, cards with ratings and quick add.
Product page
ProGallery, options, quantity, reviews.
Quick view
ProGallery, colours and add to cart without leaving the list.