A scroll-driven card stacking component. Child elements are displayed vertically; as the user scrolls through the component, they animate upward and stack on top of each other like a card deck. Each stacked card is slightly rotated and offset for a "messy pile" look. Scrolling back reverses the animation smoothly.
The component makes itself tall enough to create scroll room (viewport height + children count x scroll-distance). A sticky inner container keeps the cards visible while the user scrolls. Scroll progress drives per-card transforms (translate + rotate) applied directly to slotted children.
x-scroll-stack
| Attribute | Type | Default | Description |
|---|
peek | number | 6 | Pixels of each stacked card that remain visible |
rotation | number | 3 | Maximum rotation in degrees for stacked cards |
scroll-distance | number | 150 | Pixels of scroll needed to stack one card |
align | string | "center" | Vertical position of the stack: "top", "center", "bottom" |
disabled | boolean | false | When present, disables scroll-driven animation |
| Property | Type | Read-only | Description |
|---|
peek | number | No | Reflects peek attribute |
rotation | number | No | Reflects rotation attribute |
scrollDistance | number | No | Reflects scroll-distance attribute |
align | string | No | Reflects align attribute |
disabled | boolean | No | Reflects disabled attribute |
stackedCount | number | Yes | Number of fully stacked cards |
progress | number | Yes | Overall stacking progress (0.0 to 1.0) |
| Method | Description |
|---|
refresh() | Re-caches child dimensions and recalculates layout. Call after dynamically changing child sizes. |
| Event | Detail | Description |
|---|
x-scroll-stack-change | { stackedCount, totalCount, progress } | Fired when the number of stacked cards changes |
x-scroll-stack-progress | { progress, stackedCount, totalCount } | Fired on scroll with current progress |
| Slot | Description |
|---|
| (default) | Child elements to be stacked. Any element works; cards, images, divs, etc. |
| Property | Default | Description |
|---|
--x-scroll-stack-peek | 6px | Peek offset between stacked cards |
--x-scroll-stack-rotation | 3deg | Maximum rotation |
--x-scroll-stack-gap | 1rem | Gap between cards in their unstacked state |
--x-scroll-stack-padding-top | 2rem | Top padding inside the sticky container |
| Part | Description |
|---|
container | The sticky inner container |
- The container has
role="region". - When
prefers-reduced-motion: reduce is active, all transitions are disabled. Cards still stack/unstack based on scroll position, but without animated transitions.
<x-scroll-stack peek="8" rotation="4" scroll-distance="200" align="center">
<div class="card">Card 1</div>
<div class="card">Card 2</div>
<div class="card">Card 3</div>
<div class="card">Card 4</div>
</x-scroll-stack>
const stack = document.querySelector('x-scroll-stack');
stack.addEventListener('x-scroll-stack-change', (e) => {
console.log('Stacked:', e.detail.stackedCount, '/', e.detail.totalCount);
});
// After changing child sizes
stack.refresh();