Atelier UI®

DocsCatalogShader StudioPricingGithub
Docs 1.0.0

tools

  • Browse Catalog
  • Shader Studio
    pro
  • Collage
    new

Documentation

  • How it works
  • License
  • MCP

Page Transition (04)

  • Clip Transition
  • Stripe Transition
  • Pixel Transition
  • Band Transition

Components (36)

  • Orbit Gallery
  • Sphere Gallery
  • Spiral Gallery
  • Glowing Fog
  • Gradient Flow
  • Halftone Glow
  • Scattered Grid
  • Tag Cloud
  • Edge Bounce
  • Fluid Distortion
  • Image Trail
  • Lens Media
  • Liquid Media
  • Magnetic Dot Grid
  • Pixel Media
  • Pixel Trail
  • Dither Cursor
  • Hover Burst
  • Image Bloom
  • Curve Media
  • Infinite Gallery
  • Infinite Parallax
  • Infinite Zoom
  • Pixel Scroll
  • Scattered Scroll
  • Elastic Stick
  • Letter Swarm
  • Magnify Trail
  • Stacking Grid
  • Wavy Scroll
  • Pixelated Text
  • Text Bounce
  • Text Fluid
  • Text Scramble
  • Falling Text
  • Text Roll

Foundation Blocks (07)

  • Smooth Scroll
  • Text Split
  • WebGL Image
  • WebGL Provider
  • WebGL Scene
  • WebGL Text
  • WebGL Video
Atelier UI 1.0.0 ©2026
Star on githubBuy me a coffeellms.txt
  1. Docs
  2. /
  3. Components
  4. /
  5. Infinite Parallax

Infinite Parallax

Composable infinite parallax column effect with auto-scroll and repeating content.

https://atelier-ui.com/infinite-parallax
auto-scroll-speed
0.02
parallax-amount
2.0
auto-scroll-speed
0.02
parallax-amount
2.0
Prompt
Add Atelier's Infinite Parallax to my app.

If there is no components.json, run: npx shadcn@latest init -d
Then run: npx shadcn@latest add @atelier/infinite-parallax
That writes the atelier-ui skill under .agents/skills and .claude/skills. Follow it.

Props: autoScrollSpeed={0.02} parallaxAmount={2}

Just into your favorite coding agent and let Atelier do its magic. Live props are included.


This command will install all the dependencies this component uses.

npx shadcn@latest add @atelier/infinite-parallax

Install the dependencies first, then feel free to copy the files into your project as you see fit.

npm install motion
infinite-parallax.tsx
"use client"

import { motion, useAnimationFrame, useMotionValue, useScroll, useVelocity } from "motion/react"
import { type ComponentRef, useEffect, useRef, useState } from "react"

const REVERT_THRESHOLD = 50
const PARALLAX_SCALE = 0.0001

export type InfiniteParallaxProps = {
    reversed?: boolean
    autoScrollSpeed?: number
    parallaxAmount?: number
    children?: React.ReactNode
    className?: string
}

export function InfiniteParallax({
    reversed,
    autoScrollSpeed = 0.02,
    parallaxAmount = 2,
    children,
    className,
}: InfiniteParallaxProps) {
    const offsetRef = useRef(0)
    const contentHRef = useRef(0)
    const directionRef = useRef<1 | -1>(-1)
    const containerRef = useRef<ComponentRef<"div">>(null)
    const measureRef = useRef<ComponentRef<"div">>(null)
    const [clones, setClones] = useState(1)

    const y = useMotionValue(0)
    const { scrollY } = useScroll()
    const scrollVelocity = useVelocity(scrollY)

    useEffect(() => {
        const measure = measureRef.current
        const container = containerRef.current
        if (!measure || !container) return

        const calcClones = () => {
            const contentH = measure.getBoundingClientRect().height
            const containerH = container.getBoundingClientRect().height
            if (contentH === 0) return
            contentHRef.current = contentH
            setClones(Math.ceil(containerH / contentH))
        }

        calcClones()
        const ro = new ResizeObserver(calcClones)
        ro.observe(measure)
        ro.observe(container)

        return () => ro.disconnect()
    }, [])

    useAnimationFrame((_, delta) => {
        if (!contentHRef.current) return
        const height = contentHRef.current
        const velocity = scrollVelocity.get()

        if (Math.abs(velocity) > REVERT_THRESHOLD) {
            directionRef.current = velocity > 0 ? -1 : 1
        }

        const parallax = Math.abs(velocity) * parallaxAmount * PARALLAX_SCALE
        const step = delta * (autoScrollSpeed + parallax) * directionRef.current
        let next = offsetRef.current + step

        if (next <= -height) next = 0
        else if (next >= 0) next = -height + 1

        offsetRef.current = next
        y.set(reversed ? next : -next - height)
    })

    return (
        <div
            ref={containerRef}
            style={{ position: "relative", overflow: "hidden" }}
            className={`h-full w-full ${className ?? ""}`}
        >
            <motion.div style={{ y, position: "absolute", top: 0, left: 0, right: 0 }}>
                <div ref={measureRef}>{children}</div>
                {Array.from({ length: clones }, (_, i) => (
                    <div key={i} aria-hidden>
                        {children}
                    </div>
                ))}
            </motion.div>
        </div>
    )
}
smooth-scroll.tsx
"use client"

import type { LenisOptions } from "lenis"
import { type LenisRef, ReactLenis } from "lenis/react"
import { cancelFrame, type FrameData, frame } from "motion"
import { type ReactNode, useEffect, useRef } from "react"

type SmoothScrollProps = {
    children: ReactNode
    options?: LenisOptions
}

/**
 * Smooth scroll for the whole page (for now).
 *
 * Motion is the clock: scroll, animations, and WebGL usually each run on
 * their own loop, and can fall out of sync. This provider runs them on a
 * single loop, in a fixed order, so they always move together.
 */
export function SmoothScroll({ children, options }: SmoothScrollProps) {
    const lenisRef = useRef<LenisRef>(null)

    useEffect(() => {
        function update(data: FrameData) {
            lenisRef.current?.lenis?.raf(data.timestamp)
        }
        frame.update(update, true)
        return () => cancelFrame(update)
    }, [])

    return (
        <ReactLenis root ref={lenisRef} options={{ syncTouch: true, ...options, autoRaf: false }}>
            {children}
        </ReactLenis>
    )
}

Stack as many columns as you want. Alternate their direction with reversed.

const IMAGES = [
    "https://picsum.photos/seed/atelier-1/1200/800",
    "https://picsum.photos/seed/atelier-2/1200/800",
    "https://picsum.photos/seed/atelier-3/1200/800",
]

<div className="flex h-200 gap-2 overflow-hidden">
    <InfiniteParallax>
        {IMAGES.map((src) => (
            <img key={src} src={src} className="mb-2 w-full" alt="" />
        ))}
    </InfiniteParallax>

    <InfiniteParallax reversed>
        {IMAGES.map((src) => (
            <img key={src} src={src} className="mb-2 w-full" alt="" />
        ))}
    </InfiniteParallax>

    <InfiniteParallax>
        {IMAGES.map((src) => (
            <img key={src} src={src} className="mb-2 w-full" alt="" />
        ))}
    </InfiniteParallax>
</div>

Works best with smooth scrolling. You can add Smooth Scroll at the root for that:

Root layout
import { SmoothScroll } from "@/components/smooth-scroll/smooth-scroll";

export default function RootLayout({ children }) {
  return <SmoothScroll>{children}</SmoothScroll>;
}

NameTypeDefaultDescription
childrenReactNode—Items to render in the column.
reversedbooleanfalseReverse the column's scroll direction.
autoScrollSpeednumber0.02Auto-scroll speed. 0 to disable.
parallaxAmountnumber2Parallax intensity applied while scrolling.
classNamestring—Extra classes applied to the column container.

Motion
React animation library.

Smooth Scroll (Atelier)
Smooth scrolling for the whole page.

Star on githubBuy me a coffeellms.txt