Atelier UI

(0.3.0)
Read the docsGithub
Components14

Explore components

Getting started

  • Installation
  • How to contribute
  • Code of conduct
  • Why Atelier?

Background Effect

  • Glowing Fog
    pro
  • Halftone Glow
    pro

Cursor Effect

  • Fluid Distortion
  • Image Trail
  • Liquid Touch
  • Magnetic Dot Grid
  • Pixel Trail

Text Effect

  • Pixelated Text
  • Simple Scramble

Scroll Effect

  • Elastic Stick
    pro
  • Infinite Gallery
  • Infinite Parallax
  • Infinite Zoom
  • Scattered Scroll

Atelier UI

Pages

DocsGetting startedComponentsContribute

Social

Github(opens in new tab)Support(opens in new tab)
Design & Built by Jérémie NalletIn Paris, France 🇫🇷
Atelier UI version 0.3.0 ©2026all rights reserved
  1. Component list
  2. /
  3. Scattered Scroll

Scattered Scroll

Items that spread apart horizontally while scrolling.

Motion
Tailwind CSS
Lenis
https://atelier-ui.com/scattered-scroll-demo

CLI Install

npx atelier-ui add scattered-scroll

Manual Install

npm install motion
scattered-scroll.tsx
import { type MotionValue, motion, useScroll, useTransform } from "motion/react"
import {
    Children,
    type ReactNode,
    type RefObject,
    useLayoutEffect,
    useMemo,
    useRef,
    useState,
} from "react"

export type ScatteredScrollProps = {
    scrollTargetRef: RefObject<HTMLElement | null>
    children: ReactNode
}

// (used instead of Math.random) Avoid hydration error on next.js
function seededRandom(seed: number): number {
    const x = Math.sin(seed + 1) * 10000
    return x - Math.floor(x)
}
const Item = ({
    children,
    progress,
    xValue,
    index,
    itemRef,
}: {
    children: ReactNode
    progress: MotionValue<number>
    xValue: number
    index: number
    itemRef?: RefObject<HTMLDivElement | null>
}) => {
    /**
     * Tweak options:
     * xPercent: horizontal offset between x and x (30 and 40 default).
     * rotation: random rotation between x and x (10 and 20 default).
     * yOffset: vertical offset in px (90 default).
     */
    const { xPercent, rotation, yOffset } = useMemo(
        () => ({
            xPercent: (seededRandom(index * 2) * 10 + 30) * (index % 2 === 0 ? 1 : -1),
            rotation: (seededRandom(index * 2 + 1) * 10 + 10) * (index % 2 === 0 ? 1 : -1),
            yOffset: (index % 2 === 0 ? 1 : -1) * 90,
        }),
        [index],
    )

    const yTranslate = useTransform(progress, [0, 0.5, 1], [yOffset, 0, -yOffset])
    const xTranslate = useTransform(progress, [0, 1], [xValue, -xValue])
    const rotate = useTransform(progress, [0, 1], [rotation, -rotation])
    const xPercentValue = useTransform(progress, [0, 1], [xPercent, -xPercent])

    const scatteredX = useTransform(
        [xTranslate, xPercentValue],
        ([px, percent]) => `calc(${px}px + ${percent}%)`,
    )

    return (
        <motion.div
            className="will-change-transform"
            ref={itemRef}
            style={{
                x: scatteredX,
                rotate: rotate,
                y: yTranslate,
            }}
        >
            {children}
        </motion.div>
    )
}

export default function ScatteredScroll({ children, scrollTargetRef }: ScatteredScrollProps) {
    const childrenArray = Children.toArray(children)
    const firstItemRef = useRef<HTMLDivElement>(null)
    const [xValue, setXValue] = useState(0)

    useLayoutEffect(() => {
        if (typeof window === "undefined") return

        const update = () => {
            const containerWidth = window.innerWidth * 0.5
            const itemWidth = firstItemRef.current?.offsetWidth ?? 0
            setXValue(containerWidth + itemWidth * 0.5 * childrenArray.length)
        }

        update()
        window.addEventListener("resize", update)
        return () => window.removeEventListener("resize", update)
    }, [childrenArray.length])

    const { scrollYProgress } = useScroll({
        offset: ["start start", "end end"],
        ...(scrollTargetRef ? { target: scrollTargetRef } : {}),
    })

    return (
        <>
            {childrenArray.map((child, index) => (
                <Item
                    xValue={xValue}
                    progress={scrollYProgress}
                    index={index}
                    key={index}
                    itemRef={index === 0 ? firstItemRef : undefined}
                >
                    {child}
                </Item>
            ))}
        </>
    )
}

API

PropTypeDefaultDescription
childrenReactNode—The items to animate. Any JSX element allowed.
scrollTargetRefRefObject<HTMLElement | null>—Reference to the scroll target element.

Credits

NoMoney
Re-creation of the slider on the landing page with motion.

Motion
React animation library.

Lenis
Smooth scroll library used in the demo.

On this page
  • CLI Install
  • Manual Install
  • API
  • Credits
Community
Star this projectCreate an issueEdit this page