Card Stack
loomui/card-stackFreeComponent
Cards that pin one behind another as the page scrolls, each settling behind the next.
Live preview
live · rendered by the registry
Rendered by loomui on their own page — this is the component running, not a screenshot of it.Install it
npx shadcn@latest add https://loomui.design/r/card-stack.jsonSource
1"use client"23import * as React from "react"45import { cn } from "@/lib/utils"67export interface CardStackProps extends React.ComponentProps<"div"> {8 /** Pixels from the top of the viewport where the first card pins. */9 offset?: number10 /** Pixels of the card below that stay visible once it is covered. */11 peek?: number12 /**13 * How much a fully covered card shrinks, as a fraction of its size. Off by14 * default: it only reads as depth when `peek` is large enough to show a real15 * strip of the card behind.16 */17 scale?: number18 /** How much a fully covered card fades, from 0 to 1. */19 dim?: number20 /** Pixels of scroll the last card stays pinned for once it lands. */21 tail?: number22 /** Pin the cards but leave them at full size and opacity. */23 disabled?: boolean24}2526/** Smoothstep. Eases the start and the end of the burial. */27function smooth(t: number) {28 return t * t * (3 - 2 * t)29}3031/**32 * Sticky positions against the nearest scrolling ancestor, not always the page,33 * so the measurements have to be taken against that same box.34 */35function findScrollport(node: HTMLElement) {36 let parent = node.parentElement37 while (parent) {38 const overflow = getComputedStyle(parent).overflowY39 if (overflow === "auto" || overflow === "scroll") {40 return parent41 }42 parent = parent.parentElement43 }44 return null45}4647/**48 * Each card pins a little lower than the last, so a covered one keeps a strip on49 * screen. Progress is measured from the card *after* it: a pinned card stops50 * moving, so only what is burying it knows how buried it is.51 */52export function CardStack({53 children,54 className,55 offset = 96,56 peek = 14,57 scale = 0,58 dim = 0.12,59 tail = 200,60 disabled = false,61 style,62 ...props63}: CardStackProps) {64 const ref = React.useRef<HTMLDivElement>(null)6566 React.useEffect(() => {67 const root = ref.current68 if (!root) {69 return70 }7172 const items = () =>73 Array.from(74 root.querySelectorAll<HTMLElement>("[data-slot='card-stack-item']")75 )7677 // The pin position is per card and never changes, so it is written once78 // rather than recomputed on every frame of every scroll.79 for (const [index, item] of items().entries()) {80 item.style.setProperty("--card-top", `${offset + index * peek}px`)81 }8283 if (84 disabled ||85 window.matchMedia("(prefers-reduced-motion: reduce)").matches86 ) {87 for (const item of items()) {88// … truncated
What it pulls in
Other registry items
Files it writes
More from loomui
All 91 items| Component | Registry | Kind | Access | Installs | Command |
|---|---|---|---|---|---|
| Aurora Backdropaurora-backdrop | loomui | Components | Free | no deps | |
| Bento Gridbento-grid | loomui | Components | Free | no deps | |
| Compare Slidercompare-slider | loomui | Components | Free | no deps | |
| Confetti Buttonconfetti-button | loomui | Components | Free | no deps | |
| Count Upcount-up | loomui | Components | Free | no deps | |
| Credit Cardcredit-card | loomui | Components | Free | no deps | |
| Drawerdrawer | loomui | Components | Free | 1 dep | |
| Elastic Tabselastic-tabs | loomui | Components | Free | no deps |
