Ken Burns Pan

A still picture slowly zooms and drifts inside its frame, the documentary trick that makes a photo feel like footage.

  • CSS
  • Images
  • Loop
  • Added
Langkawi at dusk

AI prompt

Make a CSS Ken Burns effect for a React hero image. Put the <img> (object-fit: cover) inside a frame with overflow hidden and a fixed aspect ratio. Animate the image with one keyframe that alternates: from scale(1) translate(0, 0) to scale(1.18) translate(-4%, -3%), over 14s, ease-in-out, infinite alternate, so it never jumps when the loop restarts. Set transform-origin near the subject (for example 60% 40%). Overlay a bottom gradient from transparent to 60% black and a caption "Langkawi at dusk" in the corner. Add will-change: transform. Pause the animation with animation-play-state: paused when the frame is off screen (IntersectionObserver) and under prefers-reduced-motion show the still image.

Source

Demo.tsx
'use client'

import { useEffect, useRef } from 'react'
import s from './demo.module.css'

export default function KenBurns({ labels }: { labels: Record<string, string> }) {
  const ref = useRef<HTMLElement>(null)

  useEffect(() => {
    const el = ref.current
    if (!el) return
    const io = new IntersectionObserver(([e]) => el.toggleAttribute('data-paused', !e.isIntersecting))
    io.observe(el)
    return () => io.disconnect()
  }, [])

  return (
    <figure ref={ref} className={s.frame}>
      <div className={s.scene} role="img" aria-label={labels.caption} />
      <figcaption className={s.caption}>{labels.caption}</figcaption>
    </figure>
  )
}
demo.module.css
.frame {
  --sky-top: oklch(42% 0.09 300);
  --sky-low: oklch(70% 0.14 45);
  --sun: oklch(88% 0.11 75);
  --sea: oklch(36% 0.07 250);
  --island: oklch(22% 0.03 280);
  --shade: oklch(0% 0 0 / 0.6);
  position: relative;
  width: min(300px, 100%);
  aspect-ratio: 16 / 10;
  max-width: 100%;
  margin: 0;
  overflow: hidden;
  border-radius: 14px;
}

.scene {
  position: absolute;
  inset: 0;
  background:
    radial-gradient(ellipse 22% 14% at 30% 62%, var(--island) 97%, transparent 100%),
    radial-gradient(ellipse 34% 18% at 78% 64%, var(--island) 97%, transparent 100%),
    linear-gradient(180deg, transparent 62%, var(--sea) 62%),
    radial-gradient(circle at 58% 56%, var(--sun) 0 10%, transparent 10.5%),
    linear-gradient(180deg, var(--sky-top), var(--sky-low) 62%);
  transform-origin: 60% 40%;
  will-change: transform;
  animation: drift 14s ease-in-out infinite alternate;
}

.frame[data-paused] .scene { animation-play-state: paused; }

.frame::after {
  content: '';
  position: absolute;
  inset: 50% 0 0;
  background: linear-gradient(transparent, var(--shade));
}

.caption {
  position: absolute;
  left: 12px;
  bottom: 10px;
  z-index: 1;
  font: 500 var(--text-sm) / 1.2 var(--font-body);
  color: var(--stage-ink);
}

@keyframes drift {
  from { transform: scale(1) translate(0, 0); }
  to { transform: scale(1.18) translate(-4%, -3%); }
}

@media (prefers-reduced-motion: reduce) {
  .scene { animation: none; }
}

Suggest an idea

Seen a motion or a theme you would like to find here? Describe it, and it opens as a prefilled GitHub issue for you to post.

What are you suggesting?

Needs a GitHub account. Suggestions are public.