# Scroll Expand

> A rounded media frame that grows to full bleed as it scrolls through the viewport.

A rounded media frame that grows to full bleed as you scroll. Scroll inside the demo: the frame opens up, the media zooms back to 1:1, the title lifts away, and the slot content fades in once the media fills the stage.

<demo-scroll-expand>



</demo-scroll-expand>

## Installation

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

## Usage

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

<template>
  <ScrollExpand
    src="https://picsum.photos/seed/scroll-expand/1600/1000"
    alt="Mountain range at sunrise"
    title="Built to scale"
    scroll-hint="Scroll inside the frame"
    :start-width="42"
    :start-height="58"
    :start-radius="24"
    :media-zoom="1.35"
    :scroll-distance="1.2"
    :overlay-scrim="0.45"
  >
    <h2>Every pixel, everywhere</h2>
  </ScrollExpand>
</template>
```

By default the component is its own scroller, so give it a parent with a real height. Set `use-window-scroll` to drive the expansion from the page scroll instead; the frame then pins to the viewport while the page scrolls past it.

## Props

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

<tbody>
  <tr>
    <td>
      <code>
        src
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      required
    </td>
    
    <td>
      Image or video URL shown inside the frame.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        media-type
      </code>
    </td>
    
    <td>
      <code>
        'image' | 'video'
      </code>
    </td>
    
    <td>
      <code>
        'image'
      </code>
    </td>
    
    <td>
      Whether the source is an image or a looping muted video.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        poster
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Poster frame used while a video loads.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        alt
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Alt text for the image.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        title
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Headline held over the frame that lifts away as the media takes over.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scroll-hint
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Small cue shown under the resting frame that fades away as soon as the scroll begins.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        start-width
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        42
      </code>
    </td>
    
    <td>
      Frame width before expanding, as a percentage of the stage.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        start-height
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        58
      </code>
    </td>
    
    <td>
      Frame height before expanding, as a percentage of the stage.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        start-radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        24
      </code>
    </td>
    
    <td>
      Corner radius of the resting frame, in px.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        end-radius
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0
      </code>
    </td>
    
    <td>
      Corner radius once fully expanded, in px.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        media-zoom
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1.35
      </code>
    </td>
    
    <td>
      How far the media is zoomed in at rest. Eases back to 1 as the frame opens up.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        scroll-distance
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        1.2
      </code>
    </td>
    
    <td>
      Scroll length of the expansion, in multiples of the stage height.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        hold-distance
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.35
      </code>
    </td>
    
    <td>
      Extra scroll the frame stays pinned at full bleed before releasing.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        smoothing
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.1
      </code>
    </td>
    
    <td>
      Follow time in seconds. <code>
        0
      </code>
      
       locks the frame exactly to the scrollbar.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        overlay-scrim
      </code>
    </td>
    
    <td>
      <code>
        number
      </code>
    </td>
    
    <td>
      <code>
        0.45
      </code>
    </td>
    
    <td>
      Strength of the gradient scrim that fades in to keep overlay content readable.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        use-window-scroll
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        false
      </code>
    </td>
    
    <td>
      Drive the expansion from the page scroll instead of the component's own scroller.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        enabled
      </code>
    </td>
    
    <td>
      <code>
        boolean
      </code>
    </td>
    
    <td>
      <code>
        true
      </code>
    </td>
    
    <td>
      Enable or disable the expansion.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        class
      </code>
    </td>
    
    <td>
      <code>
        string
      </code>
    </td>
    
    <td>
      <code>
        ''
      </code>
    </td>
    
    <td>
      Extra classes for the root element.
    </td>
  </tr>
  
  <tr>
    <td>
      <code>
        style
      </code>
    </td>
    
    <td>
      <code>
        CSSProperties
      </code>
    </td>
    
    <td>
      —
    </td>
    
    <td>
      Inline styles for the root element.
    </td>
  </tr>
</tbody>
</table>

## Slots

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

<tbody>
  <tr>
    <td>
      <code>
        default
      </code>
    </td>
    
    <td>
      Content that fades in over the media once it reaches full bleed.
    </td>
  </tr>
</tbody>
</table>

## Accessibility

- Give the image meaningful `alt` text when it carries meaning; leave it empty for decoration.
- The expansion follows native scrolling, so mouse wheel, touch, trackpad and keyboard scrolling all work.
- With `prefers-reduced-motion: reduce` the frame locks exactly to the scroll position (smoothing is skipped).
- Videos play muted and inline, with no sound to surprise the reader.
- Ported from [React Bits](https://reactbits.dev/animations/scroll-expand) by David Haz.
