# Product Grid (Moonarc)

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.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/product-grid` (license key required)
- Tier B · category section · trigger hover, click
- Readout: `<ProductGrid kinds={[{ value, label }]} products={[{ href, name, kind, art }]} />`
- Browser support: newly (Chrome 137 · Firefox 144 · Safari 18.4); elsewhere: without view transitions the cards change places at once; without match-element the grid cross-fades
- Measured cost: 0 B JS of its own · uses FilterGrid + runtime + morph (with dependencies 3.4 kB raw; CSS 19.0 kB raw)
- Page: https://moonarc.dev/components/product-grid/

## Props

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

## Usage

```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>
```

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

## Craft

- Built on core FilterGrid, so the filtering, its URL and its view transition are the primitive's, tested there.
- The columns follow the section's own width (container queries), so the grid is right in a sidebar layout as well as full width.
- The cards are the same as ProductRail's, so a product looks the same in a row on the home page and in the shop.

## Replaces

- Collection pages (Shopify themes)
- Filterable product grids (Isotope)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/product-grid` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: filter-grid.
