Ken Burns Pan on Scroll Without JavaScript
The Ken Burns effect — a slow zoom and drift across a still image — is normally driven by a clock, which makes it decorative motion that runs whether or not anyone is watching. Tying it to a view timeline instead makes it a function of the reader’s own scrolling: the image drifts as it crosses the viewport and stops when they stop. That is both cheaper and considerably less annoying. This guide covers the sizing, the range and the property choice. It sits under scroll-driven media and gallery effects.
When to use this approach
- Hero and section-break imagery — a backdrop that should feel alive without competing with the text over it.
- Full-bleed photography in long-form articles — the drift gives a large image some presence as the reader passes it.
- Anywhere a time-based Ken Burns is already in use — converting it to a view timeline removes the “moving for nobody” problem entirely, and removes a running animation from every page load.
Avoid it on images the reader is expected to study, on anything with fine detail or text in it, and on pages that already carry parallax — the two effects compound into more relative movement than either intends.
Implementation
1. Build the frame and oversize the image
.pan-frame {
position: relative;
overflow: hidden; /* clips the oversized image */
aspect-ratio: 21 / 9;
}
.pan-frame img {
position: absolute;
inset: 0;
inline-size: 100%;
block-size: 100%;
object-fit: cover;
will-change: transform; /* only while the effect is active */
}
object-fit: cover means the image already fills the frame at scale 1, so the extra scale in the animation is pure headroom for the translate.
2. Keep the translate inside the headroom
The scale creates a margin of (scale - 1) / 2 on each side. A translate larger than that margin pulls an edge into the frame.
@keyframes ken-burns {
from { scale: 1.08; translate: 0 -1.5%; }
to { scale: 1.02; translate: 0 1.5%; }
}
At scale 1.08 the margin is 4% per side, so a 1.5% translate is comfortably inside it. Ending at 1.02 rather than 1 keeps a small margin at the end of the movement as well.
3. Range it across the whole visible span
@supports (animation-timeline: view()) {
.pan-frame img {
animation: ken-burns linear both;
animation-timeline: view(block);
animation-range: cover 0% cover 100%;
}
}
4. Use a transform, never background-position
If the image is a CSS background rather than an <img>, move it to an <img> or a pseudo-element that can be transformed. Animating background-position repaints the element every frame, and on a full-bleed hero that repaint covers the viewport.
5. Reset for reduced motion
@media (prefers-reduced-motion: reduce) {
.pan-frame img {
animation: none;
scale: 1.02; /* hold a static crop rather than snapping to 1 */
translate: 0;
}
}
Holding a slight scale keeps the composition the designer chose rather than revealing a differently-framed image to reduced-motion readers.
Verification
Scroll the section slowly from before the image enters to after it leaves and watch the frame edges. Any sliver of background appearing at any point means the translate exceeded the scale headroom — reduce the translate rather than increasing the scale, since more scale means more texture memory.
Record a scroll through the section in the Performance panel and confirm there are no Paint entries. A repaint per frame means the effect is still driven by background-position somewhere, or the image is not on its own composited layer.
Check the effect at a mobile viewport, where the frame’s aspect ratio usually changes and the same percentage translate covers a different physical distance. And confirm the reduced-motion state shows a sensible static crop rather than a jump to an unframed position.
Edge cases and gotchas
Scale increases texture memory. A layer rasterised at 1.08 scale holds proportionally more pixels. On a full-viewport hero at a high device pixel ratio this is the largest single texture on the page, and stacking two such heroes on one page is enough to matter on mid-range phones.
will-change left on permanently. A standing hint holds the layer for the whole session whether the image is on screen or not. Scope it to the section, or rely on the running animation to promote the element and drop the hint entirely.
Percentage translates resolve against the element, not the frame. translate: 0 1.5% on an image sized to the frame is 1.5% of the image’s own height. When the image and frame differ in size, the arithmetic for the safe cap changes with them.
Combining with parallax. Both effects move the same visual region. Layered together they produce more relative motion than either was budgeted for, which is the compounding problem described in vestibular-safe motion design.
An image that never finishes entering. On a short viewport a tall frame may never be fully visible, so cover progress never reaches the values the keyframes assume. Test at the shortest viewport you support, not just the narrowest.
Browser-specific notes
The transform-based pan behaves identically across engines and stays on the compositor in all three. Differences come from image handling rather than from animation.
Chromium decodes large images off the main thread more aggressively than the other engines, which makes the opening frames of a pan less likely to drop there — a difference worth knowing when a pan looks smooth in one browser and not another and nothing about the animation differs.
Safari on iOS reduces available GPU memory sharply under memory pressure and will discard layer textures, which shows as a brief blank frame in the middle of a pan. Keeping the scale modest and the served image close to its displayed size is the practical mitigation.
Firefox composites the transform as expected; its distinctive behaviour is that scroll anchoring can interact with a section whose contents grow, so reserving the frame’s aspect ratio matters slightly more there.
Frequently Asked Questions
How much scale is too much?
Beyond about 1.1 the effect starts reading as a zoom rather than a drift, and the texture cost rises with the square of the scale. Most convincing pans sit between 1.04 and 1.08, with a translate of one to two percent. The instinct to make it more visible is usually the wrong one: this effect works because it is noticed in aggregate rather than in any single moment.
Can the pan direction vary per image?
Yes, and it improves a page with several of them — a set of identically panning images reads as mechanical. Expressing the direction as two custom properties and setting them per figure keeps one keyframe set:
The keyframes reference var(--pan-x) and var(--pan-y), and each figure sets its own pair. Because the properties are only used inside a transform, they do not need @property registration to work, though registering them makes them interpolable if you later want to animate the direction itself.
Should the effect run when the image is only partly visible?
It already does — that is what the cover range means. The image begins moving as soon as any part of it enters the scrollport and finishes as the last part leaves. Narrowing the range makes the same movement happen faster, which is almost always the wrong direction for this effect.
Is this better than a time-based Ken Burns?
For a scroll page, yes, on three counts: it does not run while nobody is looking, it stops when the reader stops, and it never fights the reading position by moving while the reader is stationary. The one case where a time-based version is still correct is a genuine slideshow, where the image is the content and the reader is not scrolling at all.
Related
- Image reveal with a view timeline and clip-path — the arrival counterpart to this ambient effect
- Parallax Effects with Pure CSS — the related depth technique, and what happens when they are combined
- Compositor-Safe Properties & will-change — the layer memory this effect consumes
- Vestibular-Safe Motion Design — displacement caps for full-bleed imagery