Motion

Scroll root

A page that scrolls its own element, and the hook that finds it.

stablev0.1.0 Chromium 103 ui-scale safe

Install

Copies the file into components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.
bunx shadcn@3 add @fastrp/scroll-root

Examples

Default

Open alone

Aşağı kaydır.

Şehirler
Meslekler
Ekonomi
Çeteler
Emlak
Telefon
Etkinlikler

API

<ScrollRoot />Takes the props of what it wraps

A page that scrolls its own element instead of the window — the hub keeps `body` at `overflow: hidden` for the panel shell, and the NUI has no window scroll at all. Children find the element with `useScrollRoot()`; the motion components here use it as their viewport. It publishes its own height as `--scroll-root-h`, which the scroll-driven patterns use where they would say `100vh`: a screen of scroll is a screen of this box, and they work in a box as small as a gallery preview. `relative` is load-bearing: motion's `useScroll({ container })` measures offsets against it.

Dependencies

npm

  • None

Registry

  • @fastrp/utils

Source

components/ds/scroll-root.tsxShow
'use client';

import type * as React from 'react';
import { createContext, type RefObject, useContext, useEffect, useRef } from 'react';
import { cn } from '@/lib/utils';

// @fast-ds [email protected]

const ScrollRootContext = createContext<RefObject<HTMLElement | null> | undefined>(undefined);

/**
 * A page that scrolls its own element instead of the window — the hub keeps `body` at
 * `overflow: hidden` for the panel shell, and the NUI has no window scroll at all. Children find
 * the element with `useScrollRoot()`; the motion components here use it as their viewport.
 *
 * It publishes its own height as `--scroll-root-h`, which the scroll-driven patterns use where they
 * would say `100vh`: a screen of scroll is a screen of this box, and they work in a box as small
 * as a gallery preview. `relative` is load-bearing: motion's `useScroll({ container })` measures
 * offsets against it.
 */
function ScrollRoot({ className, children, ...props }: React.ComponentProps<'div'>) {
  const ref = useRef<HTMLDivElement>(null);

  useEffect(() => {
    const element = ref.current;
    if (!element) return;
    // A measured length in real pixels: the ui-scale pass has nothing to scale here.
    const publish = () => element.style.setProperty('--scroll-root-h', `${element.clientHeight}px`);
    publish();
    const observer = new ResizeObserver(publish);
    observer.observe(element);
    return () => observer.disconnect();
  }, []);

  return (
    <ScrollRootContext.Provider value={ref}>
      <div
        ref={ref}
        data-slot="scroll-root"
        className={cn('relative h-full overflow-y-auto overflow-x-hidden', className)}
        {...props}
      >
        {children}
      </div>
    </ScrollRootContext.Provider>
  );
}

/**
 * The nearest `ScrollRoot`'s element, or `undefined` when the window scrolls — pass it straight to
 * motion's `useScroll({ container })` or a `viewport.root`. Never substitute an empty ref for the
 * window: motion waits for a pending ref to resolve, and one that never does never tracks.
 */
function useScrollRoot(): RefObject<HTMLElement | null> | undefined {
  return useContext(ScrollRootContext);
}

export { ScrollRoot, useScrollRoot };