# Work Grid (Moonarc)

The whole body of work as a grid of covers with filters by discipline. The filters are real buttons (aria-pressed, each with its count), the order of the grid is kept, and the cards that stay slide to their places through a view transition (FilterGrid); a status line says how many projects show and is spoken after each press. A project with a clip plays it on hover (HoverVideo, caption under the clip; never under reduced motion), the others show their cover, and each card is wiped open from its foot as it enters (Reveal clip). Each cover can carry the project's view transition name, so with Astro's router it grows into the case study.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/work-grid` (license key required)
- Tier B · category section · trigger click, hover, scroll
- Readout: `<WorkGrid projects={[…]} filters={[{ label, value }]}>`
- 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; without JavaScript every card shows and the filters and the count are not offered
- Measured cost: 443 B raw JS · 317 B gzip · + Reveal + runtime + HoverVideo + FilterGrid + morph (with dependencies 5.8 kB raw; CSS 19.4 kB raw)
- Page: https://moonarc.dev/components/work-grid/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `projects` | `{ href: string; title: string; client?: string; year?: string | number; disciplines: string[]; cover: string | { src, srcset?, width?, height? }; clip?: string | string[]; name?: string }[]` | none | The projects in the grid's order. disciplines are the filter values a project answers to; clip (webm first, then mp4) plays on hover with the cover as its poster; name is the cover's view transition name, written as Astro's transition:name writes it, the same the case study's hero carries. |
| `filters` | `{ label: string; value: string }[]` | none | The filter buttons, in order. Each prints how many projects carry its value, which is part of its name (Identity 3). |
| `headline` | `string` | none | The section's heading. Slot "headline" wins. |
| `eyebrow` | `string` | none | A short line above the headline, in mono. Slot "eyebrow" wins. |
| `copy` | `string` | none | A paragraph under the headline. Slot "copy" wins. |
| `level` | `1 | 2` | `1` | Heading level of the headline: 1 on the work page itself, 2 under another h1. |
| `all` | `string | false` | `'All'` | Label of the everything button; false for none. |
| `filterLabel` | `string` | `'Filter by discipline'` | Accessible name of the filter group. |
| `status` | `string` | `'{n} of {total} projects'` | The count over the filters, a role="status" line spoken after each press: {n} the projects shown, {total} all of them. |
| `sync` | `boolean` | `false` | Mirror the chosen filter in the URL (?discipline=web) and read it on load. |
| `param` | `string` | `'discipline'` | The query parameter for sync. |
| `columns` | `2 | 3` | `2` | Columns on a wide screen: one on a phone, two from 40 rem of the section's width, three from 64 rem where asked. |
| `ratio` | `string` | `'4 / 3'` | Aspect ratio of the covers and the clips. |
| `playLabel` | `string` | `'Play the clip'` | Accessible name of a clip's play button, shown on touch screens and under reduced motion. |
| `tone` | `'surface' | 'muted' | 'ink' | 'brand'` | `unset (paints nothing)` | The ground: the page surface, a muted band, an always-dark ink panel, or a deep brand panel. The pressed filter is the theme's primary pair, the tone's text pair on ink and brand. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding and the grid's gaps ×0.8 / ×1 / ×1.25 through --ma-density. |

## Usage

```astro
---
import WorkGrid from '@/components/moonarc/pro/WorkGrid.astro';
---
<WorkGrid
  headline="Work"
  filters={[
    { label: 'Identity', value: 'identity' },
    { label: 'Web', value: 'web' },
    { label: 'Motion', value: 'motion' },
  ]}
  projects={[
    { href: '/work/harbour/', title: 'Harbour Lines', client: 'Harbour Transit', year: 2026, disciplines: ['identity', 'motion'], cover: '/work/harbour.webp', clip: ['/work/harbour.webm', '/work/harbour.mp4'], name: 'work-harbour' },
    { href: '/work/field-notes/', title: 'Field Notes', client: 'Field Press', year: 2025, disciplines: ['web'], cover: '/work/field-notes.webp', name: 'work-field-notes' },
  ]}
  sync
/>
```

## Reduced motion

The cards change places at once when a filter is pressed, no clip plays on hover (a play button is the only way to start one), and each card fades in where it was wiped.

## With ClientRouter

FilterGrid, HoverVideo and the count bind through the shared runtime and are released before the swap. Cards carry a view transition name of their own only where name is set, and the grid's own names exist only while a filter transition runs, so a navigation captures one element per project at most.

## Craft

- The count is a role="status" line the section's script rewrites from the hidden attribute itself (a MutationObserver), not from the click: FilterGrid changes the cards inside a view transition's update, a frame after the press.
- The grid keeps its order: a filter hides the cards that do not match, it never sorts, so a reader who knows the third project finds it third.
- Each button prints its count, so a filter that would empty the grid says so before it is pressed.
- A clip only where a project has one, with preload="none": the grid costs its covers until someone hovers, and a touch screen gets a play button instead of a hover it cannot make.
- The pressed filter is the theme's primary pair, the grid's one accent; everything else is the neutral ramp.

## Replaces

- Portfolio grids (Framer studio templates)
- FilterableGrid (Motion Primitives)
- Isotope portfolio

## Source

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