# Scroll Stack

> Cards pin and stack as you scroll, settling back into a dimmed deck with smooth, jitter-free motion.

A vertical list of cards that pin with native `position: sticky` as you scroll. Each new card slides over the last one, and the cards underneath shrink, darken and can blur, lean back or turn like a loose pile. Wheel scrolling glides smoothly and can snap the nearest card onto the stack. Scroll inside the panel, then try the presets.

<demo-scroll-stack>



</demo-scroll-stack>

## Installation

```bash
npx shadcn-vue@latest add "https://nxui.geoql.in/r/scroll-stack.json"
```

## Usage

```vue
<script setup lang="ts">
  import ScrollStack from '~/components/ui/scroll-stack/ScrollStack.vue';
  import ScrollStackItem from '~/components/ui/scroll-stack/ScrollStackItem.vue';

  function onStackComplete() {
    console.log('stacked');
  }
</script>

<template>
  <div class="h-125">
    <ScrollStack
      :item-distance="40"
      :item-stack-distance="22"
      stack-position="12%"
      :dim-amount="0.25"
      @stack-complete="onStackComplete"
    >
      <ScrollStackItem>
        <h2 class="text-2xl font-semibold">Card one</h2>
      </ScrollStackItem>
      <ScrollStackItem>
        <h2 class="text-2xl font-semibold">Card two</h2>
      </ScrollStackItem>
      <ScrollStackItem class="h-75 bg-transparent p-0">
        <img
          src="/photo.webp"
          alt=""
          class="absolute inset-0 size-full rounded-[inherit] object-cover"
        />
      </ScrollStackItem>
    </ScrollStack>
  </div>
</template>
```

By default the stack scrolls inside its own container, so give it a height. With `use-window-scroll` the cards pin as the page scrolls instead; then no parent may use `overflow: hidden`, or the cards cannot stick.

## Props

### ScrollStack

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        item-distance
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        100
      </code>
    </td>
    
    <td>
      Gap between cards before they stack, in px. Larger gaps mean more scrolling between cards
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        item-stack-distance
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        24
      </code>
    </td>
    
    <td>
      How far each card rests below the one before it in the stack, in px. <code>
        0
      </code>
      
       piles them exactly
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        stack-position
      </code>
    </td>
    
    <td>
      <code>
        number | string
      </code>
    </td>
    
    <td>
      <code>
        '15%'
      </code>
    </td>
    
    <td>
      Where the stack pins, from the top of the viewport or container. A number is px, or a <code>
        %
      </code>
      
       string
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        item-scale
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.05
      </code>
    </td>
    
    <td>
      How much a card shrinks for each card stacked on top of it
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        base-scale
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.8
      </code>
    </td>
    
    <td>
      The smallest a buried card can shrink to
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        dim-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.2
      </code>
    </td>
    
    <td>
      How much a card darkens for each card stacked on top of it, 0–1
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        blur-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Blur added for each card stacked on top, in px
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        tilt-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      How far buried cards lean back in 3D, in degrees
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        rotation-amount
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      How far buried cards turn, alternating left and right like a loose pile, in degrees
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hold-distance
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        200
      </code>
    </td>
    
    <td>
      How long the finished stack stays pinned before it scrolls away, in px of scrolling
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        smooth-scroll
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Glides mouse wheel scrolling. Touch, keyboard and scrollbar scrolling stay native
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        snap
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      When scrolling stops near a card, glides it the rest of the way onto the stack
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        use-window-scroll
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Stacks the cards as the page scrolls instead of inside its own container
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes on the stack container
    </td>
  </tr>
</tbody>
</table>

### ScrollStackItem

<table>
<thead>
  <tr>
    <th>
      Prop
    </th>
    
    <th>
      Type
    </th>
    
    <th>
      Default
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the card: its size, colors and padding
    </td>
  </tr>
</tbody>
</table>

## Events

<table>
<thead>
  <tr>
    <th>
      Event
    </th>
    
    <th>
      Payload
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        stack-complete
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Emitted when the last card lands on the stack
    </td>
  </tr>
</tbody>
</table>

## Slots

<table>
<thead>
  <tr>
    <th>
      Slot
    </th>
    
    <th>
      Description
    </th>
  </tr>
</thead>

<tbody>
  <tr>
    <td>
      <code>
        default
      </code>
      
       (ScrollStack)
    </td>
    
    <td>
      The cards, usually <code>
        ScrollStackItem
      </code>
      
       elements
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        default
      </code>
      
       (ScrollStackItem)
    </td>
    
    <td>
      Card content
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- Pinning uses native `position: sticky`, so keyboard, touch and scrollbar scrolling all work and never jitter.
- Only mouse wheel input is smoothed; it falls back to native scrolling inside nested scrollers and at either end of the stack.
- With `prefers-reduced-motion: reduce` wheel smoothing and snap gliding jump straight to the target.
- The dim layer on each card is `aria-hidden` and does not catch pointer events.
- Ported from [React Bits](https://reactbits.dev/components/scroll-stack) by David Haz.
