Cinematic but shippable
Totally amazing multimedia websites
Video heroes, scroll storytelling, audio, galleries, and Imagine-generated media — without tanking Core Web Vitals. Code patterns SuperGrok Heavy can implement cleanly.
HeavyPair this with Video & characters for Imagine 1.5 assets.
Principles of amazing (not exhausting) multimedia
- One hero motion — autoplay muted video or strong still + play button, not both fighting.
- Progressive enhancement — text and CTA work if video fails or
prefers-reduced-motion. - Lazy everything below the fold —
loading="lazy", intersection observers. - Poster frames always — never blank video boxes.
- Story over spectacle — each media block advances the narrative.
Master brief for a multimedia brand site
Multimedia site brief
Build a cinematic marketing site for a creative studio / product brand.
Mood: nocturnal, editorial, expensive
Media:
- Hero: muted looping video (provide poster + mp4/webm)
- Chapter sections with large stills + short captions
- Case study with before/after image compare
- Optional ambient audio toggle (off by default)
- Footer reel of 6 project thumbnails
Must:
- Work at 390px with touch-friendly controls
- prefers-reduced-motion: replace video with poster still
- Lazy-load all media below fold
- LCP: poster or optimized still, not full video file
- Real copy; no lorem
- One accent color; no purple mesh gradients
Technical: React + Tailwind; Hostinger-static friendly (public assets in /media).
Done: clean console, play/pause works, reduced-motion path verified.Video hero (accessible + performant)
VideoHero.tsx
import { useEffect, useRef, useState } from "react";
export function VideoHero() {
const ref = useRef<HTMLVideoElement>(null);
const [playing, setPlaying] = useState(true);
const [reduceMotion, setReduceMotion] = useState(false);
useEffect(() => {
const mq = window.matchMedia("(prefers-reduced-motion: reduce)");
setReduceMotion(mq.matches);
const onChange = () => setReduceMotion(mq.matches);
mq.addEventListener("change", onChange);
return () => mq.removeEventListener("change", onChange);
}, []);
function toggle() {
const v = ref.current;
if (!v) return;
if (v.paused) {
void v.play();
setPlaying(true);
} else {
v.pause();
setPlaying(false);
}
}
return (
<section className="relative min-h-[70vh] overflow-hidden bg-black">
{reduceMotion ? (
<img
src="/media/hero-poster.jpg"
alt="Studio work montage still"
className="absolute inset-0 h-full w-full object-cover opacity-70"
/>
) : (
<video
ref={ref}
className="absolute inset-0 h-full w-full object-cover opacity-70"
autoPlay
muted
loop
playsInline
poster="/media/hero-poster.jpg"
preload="metadata"
>
<source src="/media/hero.webm" type="video/webm" />
<source src="/media/hero.mp4" type="video/mp4" />
</video>
)}
<div className="relative z-10 mx-auto flex min-h-[70vh] max-w-5xl flex-col justify-end px-4 pb-16 sm:px-6">
<p className="text-xs font-semibold uppercase tracking-[0.2em] text-accent">
Atlas Studio
</p>
<h1 className="mt-3 max-w-2xl font-display text-4xl font-semibold text-white text-balance sm:text-6xl">
Brands that feel inevitable
</h1>
<div className="mt-8 flex flex-wrap gap-3">
<a
href="/work"
className="inline-flex h-11 items-center rounded-md bg-white px-5 text-sm font-semibold text-black"
>
View work
</a>
{!reduceMotion && (
<button
type="button"
onClick={toggle}
className="inline-flex h-11 items-center rounded-md border border-white/30 px-5 text-sm text-white"
>
{playing ? "Pause film" : "Play film"}
</button>
)}
</div>
</div>
</section>
);
}Scroll storytelling chapter
StoryChapter.tsx — sticky media + scrolling copy
export function StoryChapter({
title,
body,
mediaSrc,
mediaAlt,
}: {
title: string;
body: string;
mediaSrc: string;
mediaAlt: string;
}) {
return (
<section className="border-b border-border">
<div className="mx-auto grid max-w-6xl gap-8 px-4 py-16 lg:grid-cols-2 lg:gap-12">
<div className="lg:sticky lg:top-24 lg:self-start">
<img
src={mediaSrc}
alt={mediaAlt}
loading="lazy"
decoding="async"
className="aspect-[4/5] w-full rounded-xl object-cover border border-border"
/>
</div>
<div className="flex flex-col justify-center py-8">
<h2 className="font-display text-3xl font-semibold text-fg text-balance">
{title}
</h2>
<p className="mt-4 text-lg text-muted leading-relaxed">{body}</p>
</div>
</div>
</section>
);
}Hover gallery / project grid
ProjectGrid.tsx
type Project = {
title: string;
tag: string;
still: string;
hoverVideo?: string;
href: string;
};
export function ProjectGrid({ items }: { items: Project[] }) {
return (
<ul className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
{items.map((p) => (
<li key={p.href}>
<a
href={p.href}
className="group relative block overflow-hidden rounded-xl border border-border bg-surface"
>
<img
src={p.still}
alt=""
loading="lazy"
className="aspect-[16/10] w-full object-cover transition duration-500 group-hover:scale-[1.03]"
/>
{p.hoverVideo && (
<video
className="pointer-events-none absolute inset-0 hidden h-full w-full object-cover opacity-0 transition group-hover:opacity-100 md:block"
muted
loop
playsInline
preload="none"
onMouseEnter={(e) => void e.currentTarget.play()}
onMouseLeave={(e) => {
e.currentTarget.pause();
e.currentTarget.currentTime = 0;
}}
>
<source src={p.hoverVideo} type="video/mp4" />
</video>
)}
<div className="absolute inset-x-0 bottom-0 bg-gradient-to-t from-black/80 to-transparent p-4">
<p className="text-xs uppercase tracking-wider text-white/70">
{p.tag}
</p>
<p className="font-display text-lg text-white">{p.title}</p>
</div>
</a>
</li>
))}
</ul>
);
}Ambient audio (off by default)
AmbientToggle.tsx
import { useRef, useState } from "react";
export function AmbientToggle() {
const audioRef = useRef<HTMLAudioElement>(null);
const [on, setOn] = useState(false);
async function toggle() {
const a = audioRef.current;
if (!a) return;
if (on) {
a.pause();
setOn(false);
} else {
a.volume = 0.25;
await a.play();
setOn(true);
}
}
return (
<>
<audio ref={audioRef} loop preload="none" src="/media/ambient.mp3" />
<button
type="button"
onClick={toggle}
className="fixed bottom-4 right-4 z-40 rounded-full border border-border bg-surface/90 px-4 py-2 text-xs text-muted backdrop-blur hover:text-fg"
aria-pressed={on}
>
{on ? "Sound on" : "Sound off"}
</button>
</>
);
}Imagine → website media pipeline
| Asset | How to make with Heavy | Site use |
|---|---|---|
| Hero still / poster | Imagine image, cinematic, no text | LCP image + video poster |
| Hero motion | Image-to-video 6s loop, camera-only move | Muted autoplay background |
| Character series | Multi-ref Video 1.5 + freeze lists | Campaign / case study reels |
| Gallery set | Edit-chain from one master for consistency | Project grid stills |
Generate a hero loop
1) Create hero still:
Cinematic wide shot of a minimal black stage with a single chrome product pedestal,
soft volumetric light, no text, no logos, 16:9, photoreal editorial.
2) Animate (I2V):
Slow push-in, dust motes, light flicker subtle. No new objects. 6 seconds. Loop-friendly end.
3) Export poster JPG + compressed mp4/webm for /media
4) Wire VideoHero with prefers-reduced-motion fallbackPerformance budget (non-negotiable)
Tell Heavy this every time
Performance constraints for multimedia site:
- Hero video ≤ 4MB (webm preferred) or use streaming CDN URL
- Poster ≤ 200KB JPEG/WebP
- Fonts: 2 families, font-display: swap
- No autoplay sound
- Lazy-load all below-fold media
- prefers-reduced-motion disables decorative video
- After build: no layout shift from unstyled media (set width/height or aspect-ratio)
Verify LCP element is poster/still, not the video file.Power move
Combine with progressive video series in Video & characters, then embed the final clips. For conversion-first startups without cinema, use Startup websites instead.