Pattern

Stacked chapters

Full-screen chapters that pin and sink back as the next slides over.

betav0.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/stacked-chapters

Examples

Default

Open alone
Bir
İki
Üç

Bir StackedChapter içinde CitySlab.

Janti

API

<StackedChapter />

One full-screen chapter of a stack. From `md` the chapters pin one after another: each holds while the next slides over it, and sinks back — smaller, darker — as it is covered. On a phone they simply follow each other. Put them in a plain parent; the stacking is their own.

PropTypeDefaultNotes
classNamestring—
lastbooleanfalseThe last chapter is not covered by another, so it never sinks.

Dependencies

npm

  • motion@^12.40.0

Registry

Source

components/ds/stacked-chapters.tsxShow
'use client';

import { motion, useReducedMotion, useScroll, useTransform } from 'motion/react';
import type * as React from 'react';
import { useRef } from 'react';
import { useScrollRoot } from '@/components/ds/scroll-root';
import { cn } from '@/lib/utils';

// @fast-ds [email protected]

export type StackedChapterProps = {
  children: React.ReactNode;
  /** The last chapter is not covered by another, so it never sinks. */
  last?: boolean;
  className?: string;
};

/**
 * One full-screen chapter of a stack. From `md` the chapters pin one after another: each holds
 * while the next slides over it, and sinks back — smaller, darker — as it is covered. On a phone
 * they simply follow each other. Put them in a plain parent; the stacking is their own.
 */
function StackedChapter({ children, last = false, className }: StackedChapterProps) {
  const root = useScrollRoot();
  const reduceMotion = useReducedMotion();
  const ref = useRef<HTMLElement>(null);
  const { scrollYProgress } = useScroll({
    container: root,
    target: ref,
    offset: ['start start', 'end start'],
  });
  const scale = useTransform(scrollYProgress, [0, 1], [1, last || reduceMotion ? 1 : 0.9]);
  const dim = useTransform(scrollYProgress, [0, 1], [0, last ? 0 : 0.7]);

  return (
    <section
      ref={ref}
      data-slot="stacked-chapter"
      className="relative min-h-[var(--scroll-root-h,100vh)] supports-[height:100svh]:min-h-[var(--scroll-root-h,100svh)] md:sticky md:top-0 md:h-[var(--scroll-root-h,100vh)] md:supports-[height:100svh]:h-[var(--scroll-root-h,100svh)]"
    >
      <motion.div
        style={{ scale }}
        className={cn('relative h-full min-h-[inherit] overflow-hidden bg-ink', className)}
      >
        {children}
        <motion.div
          aria-hidden
          style={{ opacity: dim }}
          className="pointer-events-none absolute inset-0 bg-ink"
        />
      </motion.div>
    </section>
  );
}

export { StackedChapter };