drag · click · ← →
Compares a before and an after image with a line you drag between them.
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.
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
CSS 16.2 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 115 · not Firefox · Safari 26; needs scroll-timeline, view-transitions. 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.
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/case-study-hero---
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' },
]}
/>Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: scroll-expand, image-compare, lightbox.
| 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. |
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-case.ma-case__compare.ma-case__cover.ma-case__eyebrow.ma-case__fact.ma-case__facts.ma-case__head.ma-case__more.ma-case__summary.ma-case__title
slot="eyebrow"slot="copy"slot="aside"
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.
ClientRouterMade 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.
Replaces: Case study heroes (Framer portfolio templates) · ContainerScroll case study (Aceternity) · Project page headers (Awwwards studio sites). See the migration table.
drag · click · ← →
Compares a before and an after image with a line you drag between them.
A thumbnail gallery whose images open full size: the image grows out of its own thumbnail on a same-document view transition and returns to it on close, and ← → move through the set.
An image that starts narrow and rounded and opens to the full width as it scrolls into view.
Sample projects
The work as a numbered index: one line per project with the title at display size and the client, the discipline and the year in mono, the whole line a link.
Also: view transitions · how costs are measured · browser support · accessibility policy · five-minute setup · MCP for agents · this page as Markdown