Get started
Installation
npm install @iampoul/react-parallax- Zero dependencies — Only a React peer dependency required
- Performant — Single rAF loop — layers update via refs, no re-renders
- Accessible — Auto-pauses for prefers-reduced-motion
- Flexible — Scroll, pointer, or both. Per-layer speed, rotate, scale, fade, blur
- TypeScript — Full type definitions included
import { Parallax, ParallaxLayer } from "@iampoul/react-parallax"
export function Scene() {
return (
<Parallax
mode="both"
intensity={1}
springConfig={{ stiffness: 120, damping: 14 }}
onProgress={({ scrollProgress }) => {
console.log("scroll:", scrollProgress)
}}
>
{/* Background — slower, blurs at scroll edges */}
<ParallaxLayer speed={0.5} pointerStrength={20} blur={4} scrollRange="20%">
<img src="/bg.png" alt="" />
</ParallaxLayer>
{/* Foreground — faster, always slightly soft */}
<ParallaxLayer speed={-0.3} pointerStrength={40} blurBase={2} zIndex={1}>
<img src="/fg.png" alt="" />
</ParallaxLayer>
</Parallax>
)
}Examples
Demos
Scroll depth
Layers with different speed values create depth as you scroll. Positive values recede into the background, negative values come forward. As of v0.5.0, scrollRange also accepts a percentage string like "20%" to scale travel with the container height.
<Parallax mode="scroll" intensity={1} smoothing={0.1}>
<ParallaxLayer speed={0.7} scrollRange={80}>…far…</ParallaxLayer>
<ParallaxLayer speed={0.35} scrollRange={80}>…mid…</ParallaxLayer>
<ParallaxLayer speed={-0.4} scrollRange={80}>…close…</ParallaxLayer>
</Parallax>
{/* v0.5.0: scrollRange also accepts % of container height */}
{/* <ParallaxLayer speed={0.7} scrollRange="20%" /> */}Pointer tilt
pointerStrength controls how far each layer shifts in response to the cursor. Higher values create more dramatic parallax.
<Parallax mode="pointer" intensity={1.2} smoothing={0.07}>
<ParallaxLayer pointerStrength={12}>…</ParallaxLayer>
<ParallaxLayer pointerStrength={28}>…</ParallaxLayer>
<ParallaxLayer pointerStrength={50}>…</ParallaxLayer>
<ParallaxLayer pointerStrength={80}>…</ParallaxLayer>
</Parallax>Rotate, scale & fade
Combine rotate, scale, and fade on layers for richer scroll transitions.
<Parallax mode="scroll" intensity={1} smoothing={0.1}>
<ParallaxLayer speed={0.2} rotate={120} scrollRange={100} />
<ParallaxLayer speed={-0.3} rotate={-80} scale={0.3} scrollRange={100} />
<ParallaxLayer speed={0.5} fade={0.2} scrollRange={100} />
</Parallax>Blur depth-of-fieldv0.2.0
The blur prop applies a dynamic blur() CSS filter — sharp at scroll center, soft toward the edges. Like a camera pulling focus.
<Parallax mode="scroll" intensity={1} smoothing={0.1}>
{/* Far — blurs out at scroll edges */}
<ParallaxLayer speed={0.6} blur={10} scrollRange={90} />
{/* Mid — subtle blur */}
<ParallaxLayer speed={0.25} blur={5} scrollRange={90} />
{/* Near — always sharp */}
<ParallaxLayer speed={-0.3} blur={0} scrollRange={90} />
</Parallax>Always-soft backgroundv0.2.0
blurBase applies a constant blur at all scroll positions — perfect for soft atmospheric backgrounds. Composes with blur: total = blurBase + |scroll| × blur.
<Parallax mode="both" intensity={1} smoothing={0.09}>
{/* Always blurred — soft atmospheric background */}
<ParallaxLayer speed={0.5} blurBase={5} scrollRange={80}>
<div className="size-40 rounded-full bg-foreground/10" />
</ParallaxLayer>
{/* Blurred + gets softer toward scroll edges */}
<ParallaxLayer speed={0.2} blurBase={3} blur={4} scrollRange={80}>
<div className="size-24 rounded-3xl bg-foreground/20" />
</ParallaxLayer>
{/* Sharp focal point */}
<ParallaxLayer speed={-0.15} scrollRange={80}>
<div className="size-8 rounded-lg bg-foreground" />
</ParallaxLayer>
</Parallax>Overflow visiblev0.4.0
By default the container clips children with overflow: hidden. Set overflow="visible" to let layers drift outside the container bounds — great for floating elements that extend beyond their section.
{/* overflow="visible" lets layers drift outside container bounds */}
<Parallax
mode="pointer"
overflow="visible"
smoothing={0.07}
>
{/* bleeds left */}
<ParallaxLayer pointerStrength={60} className="absolute -left-8 top-1/2">…</ParallaxLayer>
{/* bleeds right */}
<ParallaxLayer pointerStrength={-50} className="absolute -right-8 top-1/2">…</ParallaxLayer>
{/* center — counter-drifts */}
<ParallaxLayer pointerStrength={-20}>…</ParallaxLayer>
</Parallax>Horizontal scrollv0.7.0
Set direction="horizontal" to drive scroll progress left-to-right instead of top-to-bottom — perfect for side-scrolling timelines, carousels, and panoramas. Pair with axis="x" on layers and scrollParent pointing to the scrollable wrapper.
const wrapperRef = useRef(null)
const [scrollEl, setScrollEl] = useState(null)
useEffect(() => { setScrollEl(wrapperRef.current) }, [])
<div ref={wrapperRef} style={{ overflowX: "auto" }}>
<Parallax
direction="horizontal"
mode="scroll"
scrollParent={scrollEl}
overflow="visible"
style={{ width: "300%" }}
>
<ParallaxLayer speed={0.6} axis="x" scrollRange={300}>…far…</ParallaxLayer>
<ParallaxLayer speed={0.25} axis="x" scrollRange={300}>…mid…</ParallaxLayer>
<ParallaxLayer speed={-0.4} axis="x" scrollRange={300}>…close…</ParallaxLayer>
</Parallax>
</div>Spring physicsv0.5.0
springConfig replaces the lerp smoothing with spring physics — layers overshoot and settle naturally. Lower stiffness = looser spring; lower damping = more bounce.
{/* springConfig replaces smoothing with spring physics */}
<Parallax
mode="pointer"
springConfig={{ stiffness: 80, damping: 10 }}
>
<ParallaxLayer pointerStrength={40}>…</ParallaxLayer>
<ParallaxLayer pointerStrength={80}>…</ParallaxLayer>
</Parallax>onProgress callbackv0.5.0
The onProgress prop fires every animation frame with the current ParallaxState. Drive external UI, counters, or custom animations without needing the useParallax() hook.
<Parallax
mode="both"
onProgress={({ scrollProgress, pointerX, pointerY }) => {
// Fires every rAF frame — update external UI directly
progressBar.style.width = `${scrollProgress * 100}%`
hud.textContent = `x:${pointerX.toFixed(2)} y:${pointerY.toFixed(2)}`
}}
>
<ParallaxLayer speed={0.4} pointerStrength={30}>…</ParallaxLayer>
<ParallaxLayer speed={-0.3} pointerStrength={55}>…</ParallaxLayer>
</Parallax>Live playground
Tweak the <Parallax> container props in real time. The code block reflects your current settings.
<Parallax> props
<Parallax
mode="both"
intensity={1.0}
smoothing={0.10}
disabled={false}
>API Reference
Props
<Parallax>
The container element that tracks scroll and pointer movement and broadcasts it to all child layers via a ref-based subscription — no re-renders.
| prop | type | default | description |
|---|---|---|---|
mode | "scroll" | "pointer" | "both" | "both" | Which inputs drive the parallax effect |
direction | "vertical" | "horizontal" | "vertical" | v0.7.0 — Scroll axis that drives progress. Use "horizontal" for side-scrolling layouts — pairs with axis="x" on layers |
intensity | number | 1 | Global movement multiplier — 0.5 subtle, 2 dramatic |
smoothing | number | 0.12 | Lerp easing factor — lower is floatier, 1 is instant. Ignored when springConfig is set |
springConfig | { stiffness?: number; damping?: number } | — | Spring physics alternative to lerp — gives natural overshoot. stiffness default 120, damping default 14 |
onProgress | (state: ParallaxState) => void | — | Callback fired every frame with current scroll/pointer state — drive external UI without useParallax() |
disabled | boolean | false | Pause all motion. Auto-enabled for prefers-reduced-motion |
scrollParent | HTMLElement | null | window | Custom scroll container — use when Parallax lives inside a scrollable div, modal, or sidebar |
overflow | string | "hidden" | CSS overflow on the container — set to "visible" to let layers bleed outside bounds |
as | ElementType | "div" | HTML element to render as the container |
className | string | — | CSS class for the container element |
style | CSSProperties | — | Inline styles for the container element |
<ParallaxLayer>
A single layer inside a container. Drop in as many as needed. Each layer independently controls its depth, motion axis, rotation, scale, and fade.
| prop | type | default | description |
|---|---|---|---|
speed | number | 0.3 | Depth factor — 0 locked, >0 slower/background, <0 faster/foreground |
pointerStrength | number | 0 | Pixels of travel from pointer movement |
scrollRange | number | string | 120 | Max scroll travel — absolute px or percentage of container height/width e.g. "25%". Resolves against width when direction="horizontal" |
axis | "x" | "y" | "both" | "y" | Which axis scroll parallax applies to. Use "x" with direction="horizontal" |
rotate | number | 0 | Degrees of rotation applied across the scroll range |
scale | number | 0 | Extra scale applied at scroll edges (0.2 = +20%) |
fade | number | 1 | Opacity at scroll edges — 1 means no fade |
blur | number | 0 | Max blur in px at scroll edges, 0px at center — creates a depth-of-field focus effect |
blurBase | number | 0 | Constant blur in px always applied — for layers that should always appear out of focus. Composes with blur |
zIndex | number | 0 | Stacking order for the layer |
as | ElementType | "div" | HTML element to render as the layer |
className | string | — | CSS class for the layer element |
style | CSSProperties | — | Inline styles for the layer element |
useParallax()
Access the parallax context from custom components inside a container. For simpler use cases, prefer the onProgress prop on <Parallax>.
const { intensity, disabled, mode, subscribe, getState, containerEl } = useParallax()