Product card blocks
Two ways to show products in a store: cards in a grid, and wide cards in a
list. Each block is one .svelte file built from SvRating, SvBadge,
SvNumberInput and SvButton, with a toast from SvToaster when something
goes in the cart. Prices are integer cents formatted with Intl.NumberFormat,
and the product pictures are inline SVG tinted from the --sg-* tokens.
Installation
Add the whole group, or one block by its name (shown under each demo):
See them first with npx @svgrid/ui try blocks/product-cards. No project needed.
Product card block
Four cards, each with a color swatch row that recolors the product art, a
read-only SvRating with the review count, the price, and for products on
sale the old price struck through next to an SvBadge with the percent off.
The heart in the corner toggles the wishlist (aria-pressed), and Add to cart
shows the button's loading state, then a toast naming the color. Four columns,
two below 900 px, one below 520 px.
npx @svgrid/ui add product-cards-grid
Horizontal product card block
A list of products with the art on the left, the name, variant, a one-line
description and the stock status in the middle, and the price, an
SvNumberInput quantity and Add to cart on the right. Five or fewer units
reads "Only n left", and an out-of-stock product gets a disabled quantity and
a Sold out button. The quantity is capped at the units not already in the
cart. Below 640 px the price and buttons move under the description.
npx @svgrid/ui add product-cards-horizontal
Loading products and adding to the cart
Each block's <script module> exports its core: createProductCards() and
createProductList(). Both take a products option and fall back to the
sample products only when you pass none. The core reads products once, when
it is created. Load them in the route's server load, so prices come from your
database (db stands in for your own data layer):
// src/routes/shop/+page.server.ts
import type { PageServerLoad } from './$types'
import { db } from '$lib/server/db'
export const load: PageServerLoad = async () => {
const rows = await db.products.findMany({ where: { featured: true }, take: 4, include: { variants: true } })
return {
products: rows.map((p) => ({
id: p.id,
name: p.name,
art: p.kind,
priceCents: p.priceCents,
saleCents: p.saleCents ?? undefined,
rating: p.ratingAvg,
reviews: p.ratingCount,
variants: p.variants.map((v) => ({ id: v.id, name: v.name, tint: v.cssColor })),
})),
}
}
art picks the drawing: the grid block draws mug, lamp and dripper,
and anything else as the planter. With real photos, replace the <svg> in
the card with an <img>. tint is any CSS color; the art background and the
swatch are mixed from it.
Give the block a products prop and pass it to the core. Add to cart goes
through the onAdd option: the card's button stays in its loading state until
the promise you return settles, and a thrown error goes to onError instead:
<!-- the instance <script> of product-cards-grid.svelte -->
<script lang="ts">
import { SvRating, SvBadge, SvButton, SvToaster, toast } from '@svgrid/grid'
let { products }: { products: CardProduct[] } = $props()
const cards = createProductCards({
products,
onAdd: async ({ product, variant }) => {
const r = await fetch('/api/cart', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ id: product.id, variant: variant.id, qty: 1 }),
})
if (!r.ok) throw new Error('Cart request failed')
toast.success(`${product.name}, ${variant.name}, is in your cart`)
},
onError: () => toast.error('Could not add it. Try again.'),
})
</script>
The list block's onAdd(product, qty) works the same way, and the core only
counts the units as in the cart after the promise resolves. Its stock cap runs
in the browser, so check stock again in the endpoint, and send only ids and
quantities: read the price on the server.
// src/routes/api/cart/+server.ts
import { error, json } from '@sveltejs/kit'
import type { RequestHandler } from './$types'
import { db } from '$lib/server/db'
export const POST: RequestHandler = async ({ request, locals }) => {
const { id, variant, qty } = await request.json()
const product = await db.products.findUnique({ where: { id } })
if (!product || product.stock < qty) error(409, 'Not enough stock')
await db.cartLines.upsert({ cart: locals.cartId, product: id, variant, qty })
return json({ ok: true })
}
The wishlist toggles in the browser and reports each change through
onWishlist(product, saved); pass the ids already saved with the wishlist
option.
Headless
Every block here is a styled render of the core in its <script module>.
To draw your own markup, take a core alone with
npx @svgrid/ui add product-cards-grid --headless (it writes
product-cards-grid.svelte.ts) and spread its prop-getters on plain elements.
The swatches are native radio inputs, so the arrow keys move between colors:
<script lang="ts">
import { createProductCards } from './product-cards-grid.svelte.js'
const cards = createProductCards()
</script>
{#each cards.products as p (p.id)}
<article>
<h3>{p.name}</h3>
<fieldset>
<legend>Color: {cards.variantOf(p).name}</legend>
{#each p.variants as v (v.id)}<input {...cards.swatchProps(p, v)} />{/each}
</fieldset>
<p>{cards.format(p.saleCents ?? p.priceCents)}</p>
<button {...cards.wishProps(p)}>Wishlist</button>
<button {...cards.addProps(p)}>{cards.isAdding(p.id) ? 'Adding...' : 'Add to cart'}</button>
</article>
{/each}
See also
Live examples
- Product card block - Four product cards with color swatches that recolor the art, a read-only SvRating with review count, sale prices with an SvBadge, a wishlist heart (aria-pressed) and Add to cart with loading and a toast. 4, 2 or 1 columns.
- Horizontal product card block - List-view product cards with drawn art, a description and stock status, an SvNumberInput quantity capped at the units left and Add to cart with loading and a toast. Out of stock disables both. Stacks on phones.
Related articles
- Building an E-commerce Product Catalog Grid in Svelte - How to build a product catalog admin grid with image cells, inline price and stock edits, category filtering, and bulk updates using SvGrid.
- What's New in SvGrid Enterprise - Export, Pivot, Import, Print, and AI - A practical look at @svgrid/enterprise - pivot tables, Excel and PDF export, data import, printing, and an AI assistant. Covers when each feature earns its place and what to watch out for in production.