Tall vase
32 cm
Price: $86
A whole collection on one page, filtered by kind: the products as cards (each drawn on its plate with ProductArt, a hang tag for the price, the name as the card's link and a spec line in the mono face) in two, three or four columns by the box's width, and above them a row of filters with their counts (core FilterGrid: the cards that stay slide into place, and the URL keeps the filter). A hover or the keyboard focus on a card picks its product up and swings its tag. Without JavaScript every product shows.
The shop
32 cm
Price: $86
300 ml
Price: $24
16 cm
Price: $32
18 cm
Price: $64
90 ml
Price: $18
28 cm
Price: $72
Live, from the library itself, at full size: scroll, hover, navigate away and back. The demo is never gated.
Variants · each row writes the attribute the prop writes; the section's own CSS answers
JavaScript of its own
0 B
Uses FilterGrid and the shared runtime (1.9 kB raw, once per site) and the shared morph helper (1.1 kB raw, once per site). With those included: 3.4 kB raw.
CSS 19.0 kB raw including the base tokens. Measured from a production build, and again in CI for every change that can move it.
Browser support
Chrome 137 · Firefox 144 · Safari 18.4; needs view-transitions, match-element. Elsewhere: without view transitions the cards change places at once; without match-element the grid cross-fades.
The demo above is never gated; the source is. With a key in components.json (see account), the registry serves it like any other item:
npx shadcn@latest add @moonarc-pro/product-grid---
import ProductGrid from '@/components/moonarc/pro/ProductGrid.astro';
---
<ProductGrid
level={1}
eyebrow="The shop"
kinds={[{ value: 'vases', label: 'Vases' }, { value: 'cups', label: 'Cups' }]}
products={[
{ href: '/pieces/tall-vase/', name: 'Tall vase', kind: 'vases', spec: '32 cm', price: '$86', art: { name: 'vase', colour: 'var(--ma-block-2)', block: 1 } },
{ href: '/pieces/morning-cup/', name: 'Morning cup', kind: 'cups', spec: '300 ml', price: '$24', art: { name: 'cup', colour: 'var(--ma-block-1)', block: 2 } },
]}
>
<span slot="headline">Pieces from <em>the current firing</em></span>
</ProductGrid>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: filter-grid.
| Prop | Type | Default | Description |
|---|---|---|---|
products | { href, name, kind, spec?, price?, art: { name, colour?, accent?, block? }, transition? }[] | none | The products. kind is the value of the kind it is filtered by; art.name a ProductArt drawing; transition a view-transition-name for the picture. |
kinds | { value: string; label: string }[] | none | The filters, one a kind, in order; a kind with no product is left out, and each shows its count. |
eyebrow | string | none | A short line above the heading. |
headline | string | none | The heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour. |
copy | string | none | A line under the heading. |
allLabel | string | 'All' | The everything filter's label. |
filterLabel | string | 'Filter by kind' | Accessible name of the row of filters. |
param | string | 'kind' | The query parameter that keeps the filter in the URL. |
priceLabel | string | 'Price' | The words a screen reader hears before each price. |
level | 1 | 2 | 2 | Heading level: 1 as the shop page's own heading, 2 under another h1. |
tone | 'surface' | 'muted' | 'ink' | 'brand' | unset (paints nothing) | The section's ground; the plates keep their blocks on every tone. |
density | 'compact' | 'cozy' | 'roomy' | 'cozy' | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |
The section reads the theme contract (shadcn's variable names with the base tokens as fallback), so a pasted theme or one --ma-brand recolours it. Its class names are the override surface and stay stable; a named slot replaces the matching prop with your own markup.
.ma-fgrid__bar.ma-fgrid__grid.ma-pgrid.ma-pgrid__copy.ma-pgrid__eyebrow.ma-pgrid__head.ma-pgrid__headline
slot="eyebrow"slot="headline"
The cards change places at once when a filter is picked, and the products and tags stay where they are drawn.
ClientRouterFilterGrid binds once per swap and reads the filter back from the URL.
Replaces: Collection pages (Shopify themes) · Filterable product grids (Isotope). See the migration table.
Filter buttons over a grid of cards: the cards that stay slide to their new places, the ones that go fade out, the ones that return fade in.
A product drawn flat, for a shop slot that has no photograph yet: 29 drawings in six sets (clothes, ceramics, coffee, skincare, furniture and objects, bakery).
Price: $86
A tall vase for branches and long stems, thrown in two parts and joined at the shoulder.
9 of 24 left in this batch
Wrapped in paper and card, and sent the week after it comes out of the kiln.
The top of a product's page: the product drawn large on its plate (ProductArt), with its colourways under it as small swatches (radio buttons) that recolour the drawing in place; beside it the trail back to the shop, the name, the price, a line, how many of the batch are left, two actions, the specifications in the mono face and a few notes that open (care, delivery).
A row of products that runs off the page's edge: each drawn on its plate (ProductArt) with a hang tag for the price, the name as the card's link and a spec line in the mono face.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown