Scroll-Driven Media & Gallery Effects
Photography, video posters and card galleries are where scroll-driven animation is most visible and most easily overdone. The techniques are the same handful of compositor-safe properties used elsewhere in this section, but the constraints are different: images are large, decoding is expensive, and the visual weight of a full-bleed photograph makes displacement that would be unremarkable on a text block genuinely uncomfortable. This topic covers the media-specific patterns and the media-specific costs.
Guides in this topic
- Image reveal with a view timeline and clip-path β directional wipes that read as discovery rather than arrival.
- Horizontal scroll gallery with a scroll timeline β a row of items, an indicator, and per-item progress from one scroller.
- Ken Burns pan on scroll without JavaScript β slow scale and translate tied to visibility rather than to a clock.
- Scroll-snap and scroll timelines together β why mandatory snapping makes a progress indicator step, and what to do about it.
- Stacking cards with sticky positioning β the pinned-stack pattern and its layer cost.
- Video alternatives for scroll-driven pages β why scrubbing video on scroll rarely works, and what to build instead.
Syntax reference
Media effects use the same timeline surface as everything else; what differs is which properties are worth animating on a large raster.
/* Directional reveal β clip-path is compositor-safe and does not repaint the image */
.figure img {
clip-path: inset(0 100% 0 0);
animation: wipe linear both;
animation-timeline: view(block);
animation-range: entry 20% cover 45%;
}
@keyframes wipe { to { clip-path: inset(0 0 0 0); } }
/* Slow pan β scale and translate, never width/height or background-position */
.hero img {
animation: pan linear both;
animation-timeline: view(block);
animation-range: cover 0% cover 100%;
}
@keyframes pan {
from { scale: 1.08; translate: 0 -1.5%; }
to { scale: 1; translate: 0 1.5%; }
}
/* Gallery item progress from a horizontal scroller */
.gallery { overflow-x: auto; scroll-timeline: --gallery inline; }
.gallery .item {
animation: settle linear both;
animation-timeline: view(inline);
}
Minimal working example
A single figure that wipes into view as it crosses the scrollport, with a baseline that shows the image immediately when timelines are unavailable:
<figure class="reveal">
<img src="/media/coast.avif" alt="Cliffs above a grey sea" width="1600" height="900">
<figcaption>Approaching the headland.</figcaption>
</figure>
.reveal img {
inline-size: 100%;
block-size: auto;
aspect-ratio: 16 / 9; /* reserve the box so nothing shifts when it loads */
}
@supports (animation-timeline: view()) {
.reveal img {
clip-path: inset(0 0 100% 0);
animation: reveal-up linear both;
animation-timeline: view(block);
animation-range: entry 15% entry 85%;
}
@keyframes reveal-up { to { clip-path: inset(0 0 0 0); } }
}
@media (prefers-reduced-motion: reduce) {
.reveal img { animation: none; clip-path: none; }
}
Three things make this production-ready rather than a demo: the reserved aspect ratio, the feature query around the enhancement, and the reduced-motion reset that restores the un-clipped image rather than leaving it half-covered.
animation-range and timeline scoping
Media effects are unusually sensitive to range choice, because the subject is large. A full-bleed image occupies most of the scrollport, so its cover range is dominated by the time it spends fully visible β and an animation spread across that whole range moves imperceptibly slowly for most of it and then finishes off-screen.
Two range shapes cover almost all media work. A reveal wants a short range early in the journey: entry 15% to entry 85% completes while the subject is arriving, so it has finished by the time the reader is looking at the composition. A pan wants the whole journey: cover 0% to cover 100% spreads a small movement across the entire time the image is on screen, which is what makes it feel ambient rather than deliberate.
For galleries, view(inline) gives each item its own progress and needs no per-item ranges at all. Reaching for nth-child delays in a gallery is the signal that a scroll timeline has been used where a view timeline was wanted.
Compositor-safe properties
The compositor-safe set is the same everywhere β transform, opacity, filter, and clip-path when its shape is interpolable β but the consequences of leaving it are larger for media. A large image re-rasterised every frame is expensive in a way that a re-rasterised text block is not, simply because there are more pixels.
Three property choices matter specifically here. Animate scale and translate rather than width, height, top or background-position; the last of these is the most common mistake in parallax-style image work and repaints the entire layer on every frame. Animate clip-path between two inset() shapes with the same structure, so the interpolation is a simple numeric one. And prefer filter: brightness() to animating a colour overlayβs background-color, since the filter stays on the compositor.
Common implementation patterns
Pattern 1 β the editorial wipe. A directional clip-path reveal on a full-width figure, ranged to finish as the image settles. Works best when the direction follows the composition β a horizon revealed left to right, a portrait revealed bottom to top.
Pattern 2 β the ambient pan. A slow scale from 1.08 to 1 across the whole cover range, with a small counter-translate so the crop drifts rather than simply zooming. Total movement should stay small; the effect is meant to be noticed only in aggregate.
Pattern 3 β the settling gallery. Items in a horizontal scroller each read view(inline) and scale from 0.92 to 1 as they reach the centre. This produces a focus effect with no JavaScript and no index arithmetic, and it degrades to a plain row of equal-sized items.
Pattern 4 β the pinned stack. Cards pinned with position: sticky while a view timeline scales and dims the ones behind. Visually striking, and the most expensive pattern here in layer terms, because every pinned card holds a composited layer for the duration.
Browser support and @supports guard
All the patterns in this topic sit behind the same guard as the rest of the siteβs scroll-driven work.
.figure img { clip-path: none; } /* baseline: fully visible */
@supports (animation-timeline: view()) {
.figure img { /* the enhancement */ }
}
clip-path with inset() is supported everywhere relevant and needs no separate query. Animating between clip-path shapes of different types β an inset to a polygon β is not interpolable and falls back to a discrete swap in every engine, so keep both endpoints the same shape function.
Gotchas and failure modes
-
Unreserved image boxes. An image without
width,heightoraspect-ratiohas no box until it loads, so everything below shifts when it arrives β and any view timeline measuring it re-derives mid-scroll. Reserve the box; this is the single highest-value fix in this topic. -
Decoding inside the animation window. A large image that begins decoding as its reveal starts drops the first frames. Start the fetch earlier, keep the decoded size close to the displayed size, and prefer modern formats.
-
Animating
background-positionfor a pan. It repaints the layer every frame. Use atransformon an oversized image inside a clipping container instead. -
Full-bleed displacement. Movement that occupies the whole visual field is the strongest vection trigger there is. Keep media displacement well inside the vestibular-safe ranges β for full-width imagery, smaller than you would use for a card.
-
Mandatory scroll-snap with a continuous indicator. Snapping moves the scroller in steps, so anything reading its progress steps too. Use proximity snapping when an indicator needs to glide.
-
Layer count in stacked-card sections. Each pinned card is a composited layer held for the whole section. Ten cards on a phone is a meaningful amount of GPU memory before anything has animated.
Performance checklist
- Reserve every media box with
aspect-ratioor explicit dimensions. - Serve images at close to their displayed size; a 4000-pixel-wide source in a 800-pixel slot costs decode time and texture memory for nothing.
- Animate only
transform,opacity,filterand same-shapeclip-path. - Count composited layers on the busiest media section at a mobile viewport.
- Confirm zero paint entries during a scroll through the section.
- Check the reduced-motion path restores un-clipped, un-scaled media rather than freezing it mid-effect.
What makes media different from the rest of this section
Everything in this topic could be described as βthe same techniques applied to imagesβ, and that description would miss the three constraints that actually shape the work.
Size. A full-bleed photograph is a large raster. Its composited layer costs width times height times four bytes times the square of the device pixel ratio, which on a phone means a single hero image can reserve more GPU memory than an entire page of text and cards. Effects that promote several large images simultaneously β a stacked-card section, a gallery with per-item transforms β accumulate that cost quickly, and the symptom when they exceed what the device can hold is not slowness but blank flashes where the image should be.
Decode timing. Text is laid out and painted from data the browser already has. An image has to be fetched, decoded and uploaded to the GPU before it can be composited, and the decode step happens on the main thread. When decoding overlaps an animationβs opening frames, those frames are dropped β and because the animation is scroll-driven, the reader experiences it as the effect starting late rather than as a slow image.
Visual weight. A card that slides 24 pixels is a subtle transition. A full-width photograph that slides the same 24 pixels moves a large fraction of the visual field, which is a materially stronger vestibular trigger. Displacement caps that are comfortable for interface elements need to be reduced further for media, and the honest test is not whether it looks good but whether it still looks good on a phone held close to the face.
None of these are reasons to avoid scroll-driven media effects. They are the reasons the same effect needs different numbers here: smaller displacements, tighter ranges, fewer simultaneously promoted elements, and reserved boxes without exception.
Choosing between the patterns
The six guides under this topic solve overlapping problems, and picking between them is mostly a question of what the media is doing on the page.
When the image is the content β an editorial photograph, a product shot, a diagram the reader is expected to study β reveal it and then leave it alone. A short clip-path wipe or a fade during entry gives the arrival some presence without asking the reader to look at a moving target while they are trying to read it. Continuous movement on an image someone is examining is actively unhelpful.
When the image is the backdrop β a hero, a section break, a texture behind text β a slow ambient pan across the whole visible range is the appropriate treatment. The movement should be small enough that no single moment reads as motion; what the reader notices is that the section feels alive rather than that something moved.
When there are many images at once β a gallery, a grid, a carousel β per-item progress from a view timeline is the pattern, and the interesting decision is the axis. A horizontal row gives you a scroller whose progress is meaningful in its own right, which is what makes an indicator worth adding. A vertical grid usually does not; each item reveals on its own and there is nothing global to indicate.
When the section is a narrative β a sequence the reader is meant to move through step by step β pinned stacking or snap-coupled effects give each step its own moment. These are the most expensive patterns in the topic and the ones most likely to fight with a reader who simply wants to get past them, so they earn their place on a landing page and rarely on a documentation page.
Frequently Asked Questions
Should scroll-driven media effects run on mobile at all?
Yes, with smaller numbers. The compositor path is if anything more valuable on mobile, where the main thread is slower and a scroll-event-driven equivalent would visibly stutter. What changes is the budget: fewer simultaneously promoted layers, smaller displacements because the visual field is proportionally larger, and stricter attention to decoded image size because both memory and bandwidth are tighter.
The pattern to avoid on mobile is not any particular effect but the accumulation of several at once β a pinned stack inside a page that also has a parallax hero and a horizontal gallery will exhaust GPU memory on a mid-range device long before any individual effect is at fault.
Can these effects be applied to video?
To a video elementβs position and scale, yes β it is an ordinary box and transforms apply normally. To the videoβs playback position, no, or at least not well: scrubbing a video from scroll progress requires seeking on the main thread, and seeking is not frame-accurate or fast enough to track a scroll gesture. The scroll-scrubbed video effect that appears in showcase sites is almost always a pre-rendered image sequence rather than a video, which is a different technique with different costs.
How do these interact with lazy loading?
Reserve the box and the two cooperate well. loading="lazy" defers the fetch until the image is near the viewport, which is exactly when a view timeline is about to start measuring it β so the decode can land inside the animation window unless the loading threshold is generous. Where an effect must be smooth from its first frame, either load that image eagerly or reserve enough distance in the loading threshold that decoding finishes before the reveal begins.
Do clip-path reveals hurt accessibility?
Not inherently β the image remains in the accessibility tree and its alt text is unaffected by clipping. What does matter is the reduced-motion path: a reveal that is reset by setting animation: none while leaving the initial clip-path in place hides the image permanently for anyone with the preference set. Reset the clip as well, which is why the examples here do both.
Related
- Parallax Effects with Pure CSS β the layered-depth pattern these effects sit alongside
- Horizontal scroll timelines and the inline axis β the axis rules every gallery depends on
- Core Web Vitals for Scroll & View Transitions β why unreserved media boxes are the biggest vitals risk here
- Vestibular-Safe Motion Design β displacement caps that matter more for full-bleed media