Skip to content

Components / Sections

Product Grid

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.

Live demo

The shop

Pieces from the current firing

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

tone
density

Measured

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

Baseline · newly available

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.

Install Pro

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:

terminal
npx shadcn@latest add @moonarc-pro/product-grid
src/pages/index.astro
---
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.

Props

PropTypeDefaultDescription
products{ href, name, kind, spec?, price?, art: { name, colour?, accent?, block? }, transition? }[]noneThe 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 }[]noneThe filters, one a kind, in order; a kind with no product is left out, and each shows its count.
eyebrowstringnoneA short line above the heading.
headlinestringnoneThe heading. Slot "headline" wins and may hold an <em>, set in the emphasis face and colour.
copystringnoneA line under the heading.
allLabelstring'All'The everything filter's label.
filterLabelstring'Filter by kind'Accessible name of the row of filters.
paramstring'kind'The query parameter that keeps the filter in the URL.
priceLabelstring'Price'The words a screen reader hears before each price.
level1 | 22Heading 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.

Theming and overrides

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.

Classes

.ma-fgrid__bar.ma-fgrid__grid.ma-pgrid.ma-pgrid__copy.ma-pgrid__eyebrow.ma-pgrid__head.ma-pgrid__headline

Slots

slot="eyebrow"slot="headline"

Reduced motion

The cards change places at once when a filter is picked, and the products and tags stay where they are drawn.

With ClientRouter

FilterGrid binds once per swap and reads the filter back from the URL.

Why it is built this way

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.

newly · Chrome 137 · Firefox 144 · Safari 18.4click

Product Art

Backgrounds

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).

every browserhover
Glaze

Tall vase

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

Size
32 cm tall, 14 cm across
Clay
Speckled stoneware
Care
Wash by hand
Delivery

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).

newly · Chrome 105 · Firefox 121 · Safari 15.4click

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.

Chrome 116 · not Firefox · Safari 26hoverscroll

Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown