Innovations

Ken Burns Drift

Slow infinite zoom+pan loop. Pure CSS keyframes, no scroll, no JS. Gives a still photo a living-image feel.

Preview

Source

tsx
export interface ParallaxKenBurnsProps {
  imageSrc?: string;
  /** Loop duration in seconds. */
  duration?: number;
  eyebrow?: string;
  headline?: string;
  subhead?: string;
}

export default function ParallaxKenBurns({
  imageSrc = "/heroes/blob-portrait/woman.webp",
  duration = 22,
  eyebrow = "Parallax · 07",
  headline = "Ken Burns drift",
  subhead =
    "A slow, infinite zoom-and-pan loop. Not technically parallax — there's no scroll — but it gives a still photograph the same living-image feeling.",
}: ParallaxKenBurnsProps) {
  return (
    <section className="relative isolate flex min-h-[100svh] w-full items-center justify-center overflow-hidden bg-slate-900">
      <style>{`
        @keyframes innovationsKenBurns {
          0%   { transform: scale(1.0) translate3d(0%, 0%, 0); }
          50%  { transform: scale(1.18) translate3d(-3%, -2%, 0); }
          100% { transform: scale(1.0) translate3d(0%, 0%, 0); }
        }
        .innovations-kenburns {
          animation: innovationsKenBurns ${duration}s ease-in-out infinite;
          will-change: transform;
          transform-origin: center;
        }
        @media (prefers-reduced-motion: reduce) {
          .innovations-kenburns { animation: none; }
        }
      `}</style>
      <div
        className="innovations-kenburns absolute inset-0 -z-10"
        style={{
          backgroundImage: `url(${imageSrc})`,
          backgroundSize: "cover",
          backgroundPosition: "center",
          backgroundRepeat: "no-repeat",
        }}
      />
      <div className="absolute inset-0 -z-0 bg-gradient-to-b from-black/30 via-transparent to-black/70" />
      <div className="relative z-10 mx-auto max-w-3xl px-6 text-center text-white">
        <p className="mb-4 text-xs font-semibold uppercase tracking-[0.3em] text-white/70">
          {eyebrow}
        </p>
        <h1 className="text-balance text-4xl font-semibold leading-[1.1] tracking-tight sm:text-6xl">
          {headline}
        </h1>
        <p className="mx-auto mt-6 max-w-xl text-base leading-relaxed text-white/80 sm:text-lg">
          {subhead}
        </p>
      </div>
    </section>
  );
}
Claude Code Instructions

CLI Install

npx innovations add ken-burns

Where to use it

Not technically parallax — but it solves the same problem (a still hero photo feels dead). The image slowly zooms and pans on a 22-second loop. Pure CSS, zero JS. Honors prefers-reduced-motion (animation pauses). WHEN TO USE: - Top-of-page hero where the image needs to feel alive but you don't want scroll-coupled motion - Background for testimonial sections, video lightboxes, modals - Pages where you can't budget the JS for a real scroll listener TUNING: - duration prop (seconds) — slower = more cinematic. Default 22. Don't go below 12; it starts to feel like a fidget. - Adjust scale/translate range inside the keyframes if 18% zoom is too aggressive for your image.