Totally amazing multimedia websites

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>
  );
}
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

AssetHow to make with HeavySite use
Hero still / posterImagine image, cinematic, no textLCP image + video poster
Hero motionImage-to-video 6s loop, camera-only moveMuted autoplay background
Character seriesMulti-ref Video 1.5 + freeze listsCampaign / case study reels
Gallery setEdit-chain from one master for consistencyProject 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 fallback

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