Documentation

$scroll / $drag

Create progress-driven animations tied to gestures and scroll.

Quick Start

The property:driver syntax ties any property to a progress source:

// Collapsing toolbar
<Image height:scroll={[320, 56]} />

// Bottom sheet
val sheet = $drag-y({ anchors: [0, 0.7, 1] })
<Box height:sheet={[64, 300, 64]} />

Built-in Drivers

DriverUse Case
$scrollScroll-driven animations (collapsing toolbar, parallax)
$drag-xHorizontal drag (swipe actions, drawers)
$drag-yVertical drag (bottom sheets, pull-to-refresh)
$pinchPinch to zoom
$rotateRotation gestures
$tapTap to toggle (accordion, expandable cards)
$hoverHover effects (desktop)
$pressPress state (button feedback)
$pagerPager position (tab indicator)

Syntax Levels

Level 1: Built-in Preset

<Image height:scroll={[320, 56]} />
<Card height:tap={[80, 300]} />

Level 2: Built-in + Config

<Image height:scroll={[320, 56], { to: 250 }} />
<Sheet height:drag-y={[64, 500], { velocity: 500 }} />

Level 3: Named Driver

val sheet = $drag-y({ anchors: [0, 0.7, 1], velocity: 500 })

<Image height:sheet={[64, 300, 64]} />
<Text opacity:sheet={[1, 0.5, 0]} />
<Text fontSize:sheet={[14, 24, 14]} />

Level 4: Expression Form

val scroll = $scroll({ to: 200 })
val pinch = $pinch({ min: 1, max: 3 })

// Multiply two drivers
<Image scale={scroll([1, 1.3]) * pinch([1, 3])} />

Collapsing Toolbar Example

<LazyColumn>
  <Image
    src={show.poster}
    height:scroll={[320, 56]}
    fit="cover"
  />

  <Text
    fontSize:scroll={[32, 20]}
    opacity:scroll={[1, 0]}
  >
    {show.name}
  </Text>

  @for (episode in episodes) {
    <EpisodeCard episode={episode} />
  }
</LazyColumn>

Bottom Sheet Example

val sheet = $drag-y({ anchors: [0, 0.7, 1] })

<Box>
  <!-- Main content -->
  <Column>
    <Text>Main screen content</Text>
  </Column>

  <!-- Bottom sheet -->
  <Box
    height:sheet={[64, 300, 64]}
    background="#1a1a1a"
    cornerRadius={16}
  >
    <Column p={16}>
      <Image
        width:sheet={[48, 200, 48]}
        height:sheet={[48, 200, 48]}
        src={song.artwork}
      />
      <Text fontSize:sheet={[14, 24, 14]}>
        {song.title}
      </Text>
    </Column>
  </Box>
</Box>

Driver Properties

Named drivers expose runtime properties:

val sheet = $drag-y({ anchors: [0, 0.7, 1] })

sheet.progress    // 0.0 to 1.0
sheet.state       // Current anchor index (0, 1, or 2)
sheet.snapTo(1)   // Animate to anchor 1
sheet.jumpTo(0)   // Instant jump to anchor 0

<Text>Sheet position: {sheet.progress}</Text>

Morphable Properties

These properties can be driven by gestures/scroll:

  • Size: width, height, size
  • Position: x, y, offset
  • Transform: scale, scaleX, scaleY, rotation
  • Appearance: opacity, alpha
  • Shape: cornerRadius, borderWidth
  • Typography: fontSize, letterSpacing
  • Color: backgroundColor, textColor

Cross-Component Sharing

Via Props

// Parent
val mainScroll = $scroll()
<LazyColumn>
  <HeroImage scroll={mainScroll} />
</LazyColumn>

// HeroImage.wh
@prop scroll: Driver
<Image height:scroll={[320, 80]} />

Via Context

// Parent
val mainScroll = $scroll()
$context('mainScroll') = mainScroll

// Deeply nested child
val scroll = $context('mainScroll')
<Image height:scroll={[320, 80]} />

Interpolation Methods

val sheet = $drag-y({ anchors: [0, 0.7, 1] })

<Image
  height:sheet={[48, 300, 48]}                    // linear (default)
  opacity={sheet.step(0.5, 1, 0)}                 // step function
  x={sheet.ease([8, 0, 8], "easeInOut")}          // eased
  scale={sheet.spring([1, 1.2, 1], { damping: 0.8 })}  // spring
/>

Expandable Card Example

<Card
  height:tap={[80, 300]}
  p={16}
  onClick={() => {}}  // Tap toggles
>
  <Text>Card Title</Text>
  <Text opacity:tap={[0, 1]}>
    Expanded content here...
  </Text>
</Card>

Progress-driven morphing gives you fine-grained control over animations tied to user input. Perfect for bottom sheets, collapsing toolbars, and interactive UI.

See Also