WebberUI

Zoom Parallax

A photo group that zooms through with the scroll — each image scales from the center at its own rate, creating a fly-through depth parallax.

Loading preview…
npx shadcn@latest add https://webberui.com/r/zoom-parallax.json

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

<ZoomParallax />

Installation

npx shadcn@latest add https://webberui.com/r/zoom-parallax.json

Or, once registries are configured in components.json, install it as @webberui/zoom-parallax.

Usage

import { ZoomParallax } from "@/components/ui/zoom-parallax";

<ZoomParallax
  images={[
    { src: "/photos/01.jpg", alt: "Ridgeline" },
    { src: "/photos/02.jpg", alt: "Bay" },
    { src: "/photos/03.jpg", alt: "Forest" },
    { src: "/photos/04.jpg", alt: "Sunrise" },
    { src: "/photos/05.jpg", alt: "City" },
    { src: "/photos/06.jpg", alt: "Lake" },
    { src: "/photos/07.jpg", alt: "Canyon" },
  ]}
>
  <h2 className="text-2xl font-bold text-white">Zoom Parallax</h2>
</ZoomParallax>

The component stretches out a scroll track of its own (h-[300vh] by default), and the stage in the middle is pinned sticky across the whole viewport; as you scroll down, every image scales from the center of the stage, with the outer images scaling faster, producing the fly-through parallax.

Nested scroll container

If the scrolling happens inside a container with overflow (rather than the window), pass that container's ref to container and override the track and stage heights with className / stageClassName:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[320px] overflow-y-auto">
  <ZoomParallax
    images={images}
    container={scrollerRef}
    className="h-[960px]"      // scroll track = several times the stage height
    stageClassName="h-[320px]" // stage = the container's visible height
  />
</div>

Props

PropTypeDefaultDescription
images(string | { src: string; alt?: string })[]Image sources; 5–7 is recommended
containerRefObject<HTMLElement | null>Ref of the nested scroll container; the window is the scroll container by default
layoutZoomParallaxSlot[]7 built-in slotsOverride the fly-through layout; when there are more images than slots, the slots are reused cyclically
smoothbooleantrueWhether to smooth the scroll progress with a spring
childrenReactNodeContent layered over the images, centered and not intercepting the pointer
aria-labelstring"Zoom parallax gallery"Accessible name for the gallery
classNamestringh-[300vh]The outer scroll track; its height determines the scrollable distance
stageClassNamestringh-screenThe pinned stage; override it with the container height for nested scrolling
imageClassNamestringApplied to each image element

ZoomParallaxSlot

FieldTypeDescription
scalenumberTarget scale factor at the end of the scroll (it always starts at 1)
widthnumberImage width, as a percentage of the stage width
heightnumberImage height, as a percentage of the stage height
xnumberHorizontal offset from the center of the stage (percentage)
ynumberVertical offset from the center of the stage (percentage)

How it works

  • Layout sizes and offsets are all expressed as "percentages of the stage", so the effect adapts whether it is full screen or inside a small nested container.
  • Every image binds its own useTransform that maps scroll progress onto an independent scaling curve; the outer images get the higher factors and fly apart faster.
  • Scaling is spring-smoothed by default (smooth), so it still eases to a stop when the wheel stops rather than snapping hard.
  • Images map onto layout in order; when there are more images than slots, the same set of positions is reused cyclically.

Accessibility

  • When the user has "reduce motion" enabled at the system level, all the images are shown as a static grid instead, with no scroll zooming at all.
  • The stage container carries role="group" and aria-label; decorative images with no alt are marked aria-hidden, while images with an alt are announced normally.

On this page