Scroll

createScroll() gives how far something is scrolled, as values that change as it scrolls. Given to an element's style, they move it with the scroll itself, with no animation of their own.

Live demo
scroll/solid2.tsx
import { createScroll, w } from "@wardic/solid/unstable/v2"
import { For } from "solid-js"

const LINES = [
  "The kiln is cooling.",
  "Twelve bowls went in this morning, glazed in lagoon blue.",
  "By evening the glaze has run and pooled where the walls meet the foot.",
  "Each one comes out a little different from the last.",
  "Two have crazed: fine lines all over, like ice on a pond.",
  "One cracked in the cooling, and goes on the shelf by the door.",
  "The rest are wrapped in paper and packed for the morning post.",
]

/** A panel's scroll, shown as a bar across its top: the bar is the scroll itself, with no animation of its own. */
export default function Story() {
  let panel!: HTMLDivElement
  // How far the panel is scrolled, from 0 to 1. Without `container`, it's the page's.
  const { scrollYProgress } = createScroll({ container: () => panel })
  return (
    <div class="sample-scroller">
      <w.div class="sample-progress" style={{ scaleX: scrollYProgress }} />
      <div class="sample-scroll" ref={panel} tabindex="0" aria-label="From the studio">
        <For each={LINES}>{(line) => <p>{line}</p>}</For>
      </div>
    </div>
  )
}
scroll/solid1.tsx
import { createScroll, w } from "@wardic/solid"
import { For } from "solid-js"

const LINES = [
  "The kiln is cooling.",
  "Twelve bowls went in this morning, glazed in lagoon blue.",
  "By evening the glaze has run and pooled where the walls meet the foot.",
  "Each one comes out a little different from the last.",
  "Two have crazed: fine lines all over, like ice on a pond.",
  "One cracked in the cooling, and goes on the shelf by the door.",
  "The rest are wrapped in paper and packed for the morning post.",
]

/** A panel's scroll, shown as a bar across its top: the bar is the scroll itself, with no animation of its own. */
export default function Story() {
  let panel!: HTMLDivElement
  // How far the panel is scrolled, from 0 to 1. Without `container`, it's the page's.
  const { scrollYProgress } = createScroll({ container: () => panel })
  return (
    <div class="sample-scroller">
      <w.div class="sample-progress" style={{ scaleX: scrollYProgress }} />
      <div class="sample-scroll" ref={panel} tabindex="0" aria-label="From the studio">
        <For each={LINES}>{(line) => <p>{line}</p>}</For>
      </div>
    </div>
  )
}
scroll/motion.tsx
import { motion, useScroll } from "motion/react"
import { useRef } from "react"

const LINES = [
  "The kiln is cooling.",
  "Twelve bowls went in this morning, glazed in lagoon blue.",
  "By evening the glaze has run and pooled where the walls meet the foot.",
  "Each one comes out a little different from the last.",
  "Two have crazed: fine lines all over, like ice on a pond.",
  "One cracked in the cooling, and goes on the shelf by the door.",
  "The rest are wrapped in paper and packed for the morning post.",
]

/** A panel's scroll, shown as a bar across its top: the bar is the scroll itself, with no animation of its own. */
export default function Story() {
  const panel = useRef<HTMLDivElement>(null)
  // How far the panel is scrolled, from 0 to 1. Without `container`, it's the page's.
  const { scrollYProgress } = useScroll({ container: panel })
  return (
    <div className="sample-scroller">
      <motion.div className="sample-progress" style={{ scaleX: scrollYProgress }} />
      <div className="sample-scroll" ref={panel} tabIndex={0} aria-label="From the studio">
        {LINES.map((line) => (
          <p key={line}>{line}</p>
        ))}
      </div>
    </div>
  )
}

What it gives

createScroll returns Motion's four values, each an accessor:

  • scrollX and scrollY: how far it's scrolled, in pixels.
  • scrollXProgress and scrollYProgress: the same, from 0 at the start to 1 at the end.

Without options it's the page's scroll. container is a scrolling element of your own, given as an accessor of its ref, as above; until the ref is there, every value reads 0.

They're set in the scroll's own event, so a value worked out from one paints in the frame that shows the scroll.

An element's way through

With a target, the progress is the target's way through the container, between two offsets, in Motion's terms: each pairs an edge of the target with an edge of the container.

let card!: HTMLElement
// 0 as the card's top meets the bottom of the view, 1 as its bottom leaves the top.
const { scrollYProgress } = createScroll({ target: () => card, offset: ["start end", "end start"] })
const y = createTransform(scrollYProgress, [0, 1], [60, -60])
<w.figure ref={card} style={{ y }} />

axis is the axis the offsets read, "y" unless it's "x".

Shaping it

A scroll value is a live value, so every live value works on it:

  • createTransform(scrollYProgress, [0, 1], ["0%", "100%"]) maps it, to any unit or colour.
  • createSpring(scrollYProgress) smooths it, so the effect trails the scroll a little.
  • createVelocity(scrollY) is how fast it's scrolling, to skew or blur with the speed.

Once it's in view

To play an animation as an element scrolls into view, rather than follow the scroll, use whileInView with viewport={{ once: true }}: see Gestures.