pattern
Hero Media
A full-width hero: artwork pinned behind a 16px frame and dimmed, with the content on top.
import { HeroMedia } from 'rost/components/hero-media/HeroMedia'
<HeroMedia media={<div className="hero-art" />}>
<h1>Creator first finance</h1>
</HeroMedia>
/* The artwork pins itself, at its own size in px: */
.hero-art { position: absolute; inset: 0; background: url(/art/hero.webp) no-repeat -236px -832px / 2568px 2608px fixed; }
@media (pointer: coarse), (prefers-reduced-motion: reduce) { .hero-art { background-attachment: scroll; } }src/components/hero-media/HeroMedia.tsx
import type { HTMLAttributes, ReactNode } from 'react'
import './hero-media.css'
export interface HeroMediaProps extends Omit<HTMLAttributes<HTMLElement>, 'children'> {
/** The artwork, behind everything and dimmed: a div with a background image, pinned with `background-attachment: fixed`. */
readonly media?: ReactNode
/** What sits on top of it, never dimmed: the title card, the client's logo. */
readonly children?: ReactNode
}
const b = 'rost-hero-media'
/**
* The hero at the top of a case study: a 630px section with a 16px frame,
* and the artwork dimmed inside it. Both the artwork and what sits on top
* are slots. Built from contracts/hero-media.contract.yaml.
*/
export function HeroMedia({ media, children, className, ...rest }: HeroMediaProps) {
return (
<header className={[b, className].filter(Boolean).join(' ')} {...rest}>
<div className={`${b}__frame`}>
<div className={`${b}__media`} aria-hidden="true">{media}</div>
<div className={`${b}__content`}>{children}</div>
</div>
</header>
)
}When to use
- Used at the top of a page that opens with artwork, like a project or an article. One per page.
When not to use
- A page that opens with words, not an image. The Paper effect suits that one better.
Behaviour
The artwork stays put and the page scrolls over it (background-attachment: fixed). Phones, tablets and prefers-reduced-motion get a normal static background instead. The dim only works as opacity and the colour underneath together. Never change one without the other.
Accessibility
- Artwork: it's a background image, so it's decoration by definition. Anything people need from it goes in the text.
- Contrast: check the text against the dimmed artwork, not the colour underneath.
- Motion: the pinned effect switches off with
prefers-reduced-motion.
Props
| Prop | Type | Values | Default |
|---|---|---|---|
| media | slot | ||
| content | slot |
Where it's used
- runceanu.com
src/rost/hero-media/HeroMedia.tsx.rost-hero-media
Where code differs on purpose
- Everywhere:The artwork is dimmed with `opacity/medium` (0.48) on a #171717 frame. Figma's node says 32% (`opacity/mild`).I set one dim for every case study on 2026-10-18. What you see is opacity times what's underneath, and three heroes looked darker at the same opacity. Figma never caught up.
- runceanu.com:Each case study makes the hero full-bleed (`width: 100vw`, negative margin) and brings its own artwork, sized and positioned for that collage.The page has side padding and the hero ignores it. Where the artwork sits is content, not the component.
Values
Straight from the contract. Each value sits next to its token, because six tokens here all come out as #3a3a3a and the colour alone can't tell you which one is right.
Never changes
| Part | Property | Value | Token |
|---|---|---|---|
| root | display | block | layout |
| root | position | relative | layout |
| root | height | 630px | spacing/layout/hero-height |
| root | padding | 16px | spacing/gap/medium |
| root | background | #060805 | non-interactive/background/primary |
| frame | position | relative | layout |
| frame | height | 100% | layout |
| frame | overflow | hidden | layout |
| frame | clip-path | inset(0 round var(--spacing-radius-xlarge)) | Figma frame size |
| frame | border-radius | 24px | spacing/radius/xlarge |
| frame | background | #171717 | non-interactive/background/secondary |
| media | position | absolute | layout |
| media | inset | 0 | layout |
| media | pointer-events | none | Figma frame size |
| media | opacity | 0.48 | opacity/medium |
| content | position | relative | layout |
| content | height | 100% | layout |