# Case Study Hero (Moonarc)

The top of a case study: the project's title at display size, its facts in a grid (client, year, role, credits), the cover that opens from a rounded inset to the full width as the page scrolls (ScrollExpand), then a before-and-after (ImageCompare) or a gallery that opens full size (Lightbox). The cover's frame carries the project's view transition name, so with Astro's router the cover a reader chose on an index (ProjectIndex, WorkGrid) grows into it; the frame is the element with the clip, so the snapshot is the cover as it looks. Zero JavaScript of its own.

- Pro block. Install: `npx shadcn@latest add @moonarc-pro/case-study-hero` (license key required)
- Tier B · category section · trigger scroll, click
- Readout: `<CaseStudyHero title="…" facts={[…]} cover={{ src, alt }} name="work-harbour">`
- Browser support: limited (Chrome 115 · not Firefox · Safari 26); elsewhere: without scroll timelines (Firefox) the cover is full width from the start; without view transitions (or Astro's router) the page opens without the cover growing
- Measured cost: 0 B JS (CSS 16.2 kB raw)
- Page: https://moonarc.dev/components/case-study-hero/

## Props

| Prop | Type | Default | Description |
|---|---|---|---|
| `title` | `string` | none | The project's title, the page's heading. |
| `level` | `1 | 2` | `1` | Heading level of the title: 1 on the case study page, 2 under another h1. |
| `eyebrow` | `string` | none | A short line over the title, in mono: the project's number, its disciplines. Slot "eyebrow" wins. |
| `summary` | `string` | none | One or two sentences under the title: what the project was. Slot "copy" wins. |
| `facts` | `{ label: string; value: string }[]` | `[]` | The facts as a description list in a grid: client, year, role, credits, as many to a row as fit. |
| `cover` | `{ src: string | { src, srcset?, width?, height? }; alt: string }` | none | The image the page opens on, loaded at the full width from a srcset; alt says what the project shows. |
| `name` | `string` | none | View transition name of the cover's frame, written as Astro's transition:name writes it: the same name an index's cover carries. |
| `expand` | `number` | `0.66` | Starting width of the cover as a share of the full width, 0.2–1. |
| `compare` | `{ before: { src, alt }; after: { src, alt }; labels?: [string, string]; label?: string; start?: number }` | none | A before-and-after under the cover (ImageCompare): the slider is a native range, named label. |
| `gallery` | `{ src: string; thumb?: string; alt: string; caption?: string }[]` | none | A gallery under the cover in place of compare (Lightbox): thumbnails that grow into the full image and step with ← →. |
| `galleryLabel` | `string` | `'Gallery'` | Accessible name of the gallery and its dialog. |
| `prevLabel` | `string` | `'Previous image'` | Accessible name of the gallery's previous button. |
| `nextLabel` | `string` | `'Next image'` | Accessible name of the gallery's next button. |
| `closeLabel` | `string` | `'Close'` | Accessible name of the gallery's close button. |
| `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. |
| `density` | `'compact' | 'cozy' | 'roomy'` | `'cozy'` | Block padding ×0.8 / ×1 / ×1.25 through --ma-density. |

## Usage

```astro
---
import CaseStudyHero from '@/components/moonarc/pro/CaseStudyHero.astro';
import { getImage } from 'astro:assets';
import cover from '../../assets/work/harbour/cover.webp';

const img = await getImage({ src: cover, widths: [800, 1600, 2400], width: 2400 });
---
<CaseStudyHero
  eyebrow="01 / 04 · Identity, wayfinding"
  title="Harbour Lines"
  summary="A mark, a typeface and a sign system for a city's ferries, read from a moving deck."
  facts={[
    { label: 'Client', value: 'Harbour Transit' },
    { label: 'Year', value: '2026' },
    { label: 'Role', value: 'Identity, wayfinding, motion' },
    { label: 'Credits', value: 'Design: A. Reyes · Motion: J. Okafor' },
  ]}
  cover={{ src: { src: img.src, srcset: img.srcSet.attribute }, alt: 'The harbour ferry\'s mark on a blue poster' }}
  name="work-harbour"
  gallery={[
    { src: '/work/harbour/signs.webp', alt: 'Signs on a pier, the line number in the mark\'s circle' },
    { src: '/work/harbour/map.webp', alt: 'The route map, four lines in four colours' },
  ]}
/>
```

## Reduced motion

The cover is full width from the start (ScrollExpand), Astro's router skips the view transition so the page simply opens, and the gallery opens as a plain dialog.

## With ClientRouter

Made for it: the cover's frame carries the name an index's cover carries, and the router grows one into the other. ImageCompare and Lightbox bind through the shared runtime.

## Craft

- The name is on ScrollExpand's frame, not on the image: a view transition captures an element's own clip-path but not its parent's (measured in Chromium), so the named frame is snapshotted as the reader sees it, rounded inset and all, and the grow lands on it without a jump.
- The title is set at up to 9 rem and 0.9 line height: on a case study the project's name is the page's first image.
- The facts are a description list, label over value in mono and text, in as many columns as fit: a reader looking for the year finds the word Year first.
- The cover loads at the full width from a srcset with fetchpriority high: it is what the page opens on.

## Replaces

- Case study heroes (Framer portfolio templates)
- ContainerScroll case study (Aceternity)
- Project page headers (Awwwards studio sites)

## Source

Pro block. The source is served by the license-gated registry: `npx shadcn@latest add @moonarc-pro/case-study-hero` with a key in components.json (https://moonarc.dev/account/). Everything it composes is free: scroll-expand, image-compare, lightbox.
