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-chaptersExamples
Default
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.
| Prop | Type | Default | Notes |
|---|---|---|---|
| className | string | — | |
| last | boolean | false | The last chapter is not covered by another, so it never sinks. |
Dependencies
npm
- motion@^12.40.0
Registry
- @fastrp/scroll-root
- @fastrp/utils
Source
components/ds/stacked-chapters.tsxShowHide
'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 };