npm package

react-parallax

Scroll and pointer parallax for React. One container, unlimited layers. Move your cursor.

zero depsrAF-basedTypeScripta11y ready
npm i @iampoul/react-parallaxGitHub →
v0.7.0|MIT|React ≥18

Get started

Installation

npm install @iampoul/react-parallax
  • Zero dependenciesOnly a React peer dependency required
  • PerformantSingle rAF loop — layers update via refs, no re-renders
  • AccessibleAuto-pauses for prefers-reduced-motion
  • FlexibleScroll, pointer, or both. Per-layer speed, rotate, scale, fade, blur
  • TypeScriptFull type definitions included
scene.tsx
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.

scroll to see depth
<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.

move cursor here
<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.

scroll to see transforms
<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.

scroll to pull 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.

move cursor & scroll
<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.

move cursor here
{/* 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.

move cursor here
{/* 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.

scrollProgress
0.500
pointerX
0.500
pointerY
0.500
scroll & move cursor
<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

mode
disabled
<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.

proptypedefaultdescription
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
intensitynumber1Global movement multiplier — 0.5 subtle, 2 dramatic
smoothingnumber0.12Lerp 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) => voidCallback fired every frame with current scroll/pointer state — drive external UI without useParallax()
disabledbooleanfalsePause all motion. Auto-enabled for prefers-reduced-motion
scrollParentHTMLElement | nullwindowCustom scroll container — use when Parallax lives inside a scrollable div, modal, or sidebar
overflowstring"hidden"CSS overflow on the container — set to "visible" to let layers bleed outside bounds
asElementType"div"HTML element to render as the container
classNamestringCSS class for the container element
styleCSSPropertiesInline 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.

proptypedefaultdescription
speednumber0.3Depth factor — 0 locked, >0 slower/background, <0 faster/foreground
pointerStrengthnumber0Pixels of travel from pointer movement
scrollRangenumber | string120Max 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"
rotatenumber0Degrees of rotation applied across the scroll range
scalenumber0Extra scale applied at scroll edges (0.2 = +20%)
fadenumber1Opacity at scroll edges — 1 means no fade
blurnumber0Max blur in px at scroll edges, 0px at center — creates a depth-of-field focus effect
blurBasenumber0Constant blur in px always applied — for layers that should always appear out of focus. Composes with blur
zIndexnumber0Stacking order for the layer
asElementType"div"HTML element to render as the layer
classNamestringCSS class for the layer element
styleCSSPropertiesInline 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()