Skip to content

Components / Sections

Case Study Hero

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 demo

01 / 04 · Sample case study

Harbour Lines

A mark, a typeface and a sign system for an imaginary ferry line, read from a moving deck.

Client
A sample ferry line
Year
2026
Role
Identity, wayfinding, motion
Credits
Sample names
A sample cover: a blue field with a pale circle

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

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

Limited availability

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.

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/case-study-hero
src/pages/index.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' },
  ]}
/>

Without a key the registry answers 401 with an upgrade message. Everything this section composes is free: scroll-expand, image-compare, lightbox.

Props

PropTypeDefaultDescription
titlestringnoneThe project's title, the page's heading.
level1 | 21Heading level of the title: 1 on the case study page, 2 under another h1.
eyebrowstringnoneA short line over the title, in mono: the project's number, its disciplines. Slot "eyebrow" wins.
summarystringnoneOne 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 }noneThe image the page opens on, loaded at the full width from a srcset; alt says what the project shows.
namestringnoneView transition name of the cover's frame, written as Astro's transition:name writes it: the same name an index's cover carries.
expandnumber0.66Starting 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 }noneA before-and-after under the cover (ImageCompare): the slider is a native range, named label.
gallery{ src: string; thumb?: string; alt: string; caption?: string }[]noneA gallery under the cover in place of compare (Lightbox): thumbnails that grow into the full image and step with ← →.
galleryLabelstring'Gallery'Accessible name of the gallery and its dialog.
prevLabelstring'Previous image'Accessible name of the gallery's previous button.
nextLabelstring'Next image'Accessible name of the gallery's next button.
closeLabelstring'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.

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

Slots

slot="eyebrow"slot="copy"slot="aside"

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.

Why it is built this way

Replaces: Case study heroes (Framer portfolio templates) · ContainerScroll case study (Aceternity) · Project page headers (Awwwards studio sites). See the migration table.

The scene as a line drawingThe scene in colour

drag · click · ← →

Compares a before and an after image with a line you drag between them.

newly · Chrome 124 · Firefox 121 · Safari 16.5pointerclick
Study 1Study 2Study 3

click · grows from its thumbnail · ← →

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.

newly · Chrome 111 · Firefox 144 · Safari 18click

An image that starts narrow and rounded and opens to the full width as it scrolls into view.

Chrome 115 · not Firefox · Safari 26scroll

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