# Scroll 01

> Sticky media that changes as you scroll through text content.

A shadcn/ui block from the [@flx](https://ui.flexnative.com) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/flx/scroll-01).

- Page, with a live preview: https://shadcn.exchange/r/flx/scroll-01
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/flx/scroll-01-light-ab63849c04e6c936.png
- Registry item: https://ui.flexnative.com/r/scroll-01.json
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://ui.flexnative.com/r/scroll-01.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `motion`

## Files

### `registry/blocks/scroll/scroll-01/scroll-01.tsx`

```tsx
'use client'

import {
  motion,
  useScroll,
  useTransform,
  useMotionValueEvent,
} from 'motion/react'
import { useRef, useState, Dispatch, SetStateAction } from 'react'

type Scroll01Item = {
  title: string
  description: string
  media: string
}

export interface Scroll01Props {
  items: Scroll01Item[]
}

function ScrollItem({
  item,
  index,
  setActive,
}: {
  item: Scroll01Item
  index: number
  setActive: Dispatch<SetStateAction<number>>
}) {
  const ref = useRef<HTMLDivElement | null>(null)

  const { scrollYProgress } = useScroll({
    target: ref,
    offset: ['start 90%', 'end 15%'],
  })

  const y = useTransform(scrollYProgress, [0, 1], [20, -20])

  const opacityValues = index === 0 ? [1, 0.7, 1, 0] : [0, 0.7, 1, 0]
  const opacity = useTransform(scrollYProgress, [0, 0.3, 0.7, 1], opacityValues)

  const isActive = useTransform(scrollYProgress, (v) => v > 0.4 && v < 0.6)

  useMotionValueEvent(isActive, 'change', (v) => {
    if (v) {
      setActive((prev) => (prev === index ? prev : index))
    }
  })

  return (
    <motion.article
      ref={ref}
      style={{ opacity, y }}
      className="flex flex-col items-center"
    >
      <div className="text-center">
        <h3 className="mb-2 text-2xl font-semibold">{item.title}</h3>
        <p className="text-muted-foreground">{item.description}</p>
      </div>
    </motion.article>
  )
}

export function Scroll01({ items }: Readonly<Scroll01Props>) {
  const [activeIndex, setActiveIndex] = useState<number>(0)

  return (
    <>
      <div className="space-y-10 md:hidden">
        {items.map((item, index) => (
          <article
            key={`${item.title}-${index}`}
            className="flex flex-col items-start space-y-4"
          >
            <div className="space-y-2">
              <h3 className="text-2xl font-semibold">{item.title}</h3>
              <p className="text-muted-foreground">{item.description}</p>
            </div>
            <img
              src={item.media}
              alt={item.title}
              className="h-72 w-full rounded-2xl object-cover"
            />
          </article>
        ))}
      </div>

      <div className="hidden gap-10 md:grid md:grid-cols-2">
        <div className="sticky top-20 max-h-[70vh] overflow-hidden rounded-2xl">
          {items.map((item, index) => (
            <motion.img
              key={`${item.title}-${index}`}
              src={item.media}
              alt={item.title}
              className="absolute inset-0 aspect-4/3 h-full w-full object-cover"
              initial={{ opacity: index === 0 ? 1 : 0 }}
              animate={{
                opacity: activeIndex === index ? 1 : 0,
                willChange: 'opacity',
              }}
              transition={{
                duration: 0.15,
                ease: 'linear',
              }}
            />
          ))}
        </div>

        <div className="py-[35vh]">
          <div className="space-y-[30vh]">
            {items.map((item, index) => (
              <ScrollItem
                key={`${item.title}-${index}`}
                item={item}
                index={index}
                setActive={setActiveIndex}
              />
            ))}
          </div>
        </div>
      </div>
    </>
  )
}
```

### `registry/blocks/scroll/scroll-01/scroll-01-example.tsx`

```tsx
import {
  Scroll01,
  type Scroll01Props,
} from './scroll-01'

export const values = {
  items: [
    {
      title: 'Build faster',
      description: 'Create interfaces quickly using reusable blocks.',
      media:
        'https://images.unsplash.com/photo-1486092642310-0c4e84309adb?q=80&w=2070&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    },
    {
      title: 'Customize easily',
      description: 'Adapt everything to your design system.',
      media:
        'https://images.unsplash.com/photo-1479707406242-e8929e87e734?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    },
    {
      title: 'Stay consistent',
      description: 'Keep layouts balanced with a clear visual rhythm.',
      media:
        'https://images.unsplash.com/photo-1628880689946-f4a0533ac5fc?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    },
    {
      title: 'Guide attention',
      description: 'Highlight the right content without adding noise.',
      media:
        'https://images.unsplash.com/photo-1571495653425-621ba3cb7ac1?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    },
    {
      title: 'Scale calmly',
      description: 'Expand your pages with patterns that stay elegant.',
      media:
        'https://images.unsplash.com/photo-1561990306-7462bfe923b6?q=80&w=1170&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D',
    },
  ],
} satisfies Scroll01Props

export function Scroll01Example() {
  return (
    <div>
      <div className="h-10" />
      <Scroll01 items={values.items} />
      <div className="h-30" />
    </div>
  )
}
```
