useScroll

useScroll is used to create scroll-linked animations, like progress indicators and parallax effects.

Note: When scroll-linked animations are powered by the scroll function from motion, animations using opacity or transform CSS properties can be hardware accelerated.

<script>
  import { useScroll, useSpring } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const scaleX = useSpring(scrollYProgress)
</script>

<div
  style="position: fixed; top: 0; left: 0; right: 0; height: 5px;
         background: #ff0088; transform-origin: left;
         transform: scaleX({scaleX.current});"
/>
<script>
  import { useScroll, useSpring } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const scaleX = useSpring(scrollYProgress)
</script>

<div
  style="position: fixed; top: 0; left: 0; right: 0; height: 5px;
         background: #ff0088; transform-origin: left;
         transform: scaleX({scaleX.current});"
/>

Usage

Import

<script>
  import { useScroll } from '@humanspeak/svelte-motion'
</script>
<script>
  import { useScroll } from '@humanspeak/svelte-motion'
</script>

useScroll returns four motion values augmented with a $state-backed .current getter and a Svelte readable .subscribe shim:

ValueDescription
scrollXHorizontal scroll position in pixels
scrollYVertical scroll position in pixels
scrollXProgressHorizontal scroll progress between 0 and 1
scrollYProgressVertical scroll progress between 0 and 1

Read them with scrollY.current in templates and $derived / $effect, with scrollY.get() in imperative code, or with $scrollY for store-style consumers. They compose with useTransform, useSpring, and every other motion-value-aware hook.

Page scroll

By default, useScroll tracks the page scroll position:

<script>
  import { useScroll, useSpring } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const scaleX = useSpring(scrollYProgress)
</script>

<div
  style="position: fixed; top: 0; left: 0; right: 0; height: 5px;
         background: #ff0088; transform-origin: left;
         transform: scaleX({scaleX.current});"
/>
<script>
  import { useScroll, useSpring } from '@humanspeak/svelte-motion'

  const { scrollYProgress } = useScroll()
  const scaleX = useSpring(scrollYProgress)
</script>

<div
  style="position: fixed; top: 0; left: 0; right: 0; height: 5px;
         background: #ff0088; transform-origin: left;
         transform: scaleX({scaleX.current});"
/>
file · Default.svelte mode · live running open source
// scroll-progress scroll 0%
01 ingest
02 normalize
03 index
04 rank
05 serve
useScroll → useSpring transform: scaleX

Element scroll

Track the scroll position of a specific scrollable element by passing it as the container:

<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let containerEl

  const { scrollYProgress } = useScroll({ container: containerEl })
</script>

<div bind:this={containerEl} style="overflow-y: scroll; height: 300px;">
  <!-- tall content -->
</div>

<div>Scroll progress: {scrollYProgress.current}</div>
<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let containerEl

  const { scrollYProgress } = useScroll({ container: containerEl })
</script>

<div bind:this={containerEl} style="overflow-y: scroll; height: 300px;">
  <!-- tall content -->
</div>

<div>Scroll progress: {scrollYProgress.current}</div>

Element position

Track a target element’s position as it scrolls within its container (or the viewport):

<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let targetEl

  const { scrollYProgress } = useScroll({ target: targetEl })
</script>

<div bind:this={targetEl}>
  <div style="opacity: {scrollYProgress.current}">
    Fades in as you scroll to this element
  </div>
</div>
<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let targetEl

  const { scrollYProgress } = useScroll({ target: targetEl })
</script>

<div bind:this={targetEl}>
  <div style="opacity: {scrollYProgress.current}">
    Fades in as you scroll to this element
  </div>
</div>

Scroll offsets

When tracking an element’s position, the offset option defines when the tracking starts and ends. Each offset is a pair of intersections — one for the target and one for the container:

<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let targetEl

  // Start when target enters bottom of viewport,
  // end when target reaches top of viewport
  const { scrollYProgress } = useScroll({
    target: targetEl,
    offset: ['start end', 'end start']
  })
</script>
<script>
  import { useScroll } from '@humanspeak/svelte-motion'

  let targetEl

  // Start when target enters bottom of viewport,
  // end when target reaches top of viewport
  const { scrollYProgress } = useScroll({
    target: targetEl,
    offset: ['start end', 'end start']
  })
</script>

Named offset values:

ValueDescription
"start"Top edge of the element
"center"Center of the element
"end"Bottom edge of the element

You can also use numbers (0 to 1) and pixel values ("100px").

Performance

Scroll animations work best with CSS properties that can be GPU-accelerated:

  • transform (translateX, translateY, scale, rotate)
  • opacity

These properties don’t trigger layout or paint, so the browser can animate them on the compositor thread for smooth 60fps performance even during rapid scrolling.

scrollXProgress and scrollYProgress, and values mapped from them with useTransform(progress, [input], [output]), run as native scroll-timeline animations when the browser supports them and they are bound to opacity, filter, clipPath, transform, or backgroundColor. The following fall back to a JavaScript update on every scroll event:

  • Chained transforms (a transform of another transformed value)
  • clamp: false
  • Function transformers, such as useTransform(progress, (v) => ...)
  • Input stops outside ascending 0-1 (for example, descending ranges)

The x, y, and scale shortcuts also update in JavaScript, to match Framer Motion. Map to a full transform string when you need compositor-driven movement.

Which offset values can run natively depends on whether you pass a target:

  • With a target, the progress values run on a native view timeline when the offset’s two points sit on the container’s start/end edges with proportional target positions. For example, ["start end", "end start"] (while the target is in view), ["start end", "end end"] (entering), and ["center end", "center start"]. Pixel and vw/vh lengths, and container edges such as center, fall back to JavaScript.
  • Without a target, page or container progress runs natively only when no offset is set. A scroll timeline can’t express an offset, so useScroll({ offset }) always updates in JavaScript.

A native view timeline follows the target’s nearest scrolling ancestor. If your CSS makes body a scroll container that doesn’t actually scroll (for example overflow-y: auto on both html and body), native view-timeline animations stay frozen. Leave body at overflow: visible so the document is the scroller.

Options

OptionTypeDescription
containerHTMLElementScrollable element to track. Defaults to the page.
targetHTMLElementTarget element to track position of within the container.
offsetstring[]Array of scroll offsets defining when tracking starts and ends.
axis'x' \| 'y'Which axis to use for the single-axis progress callback. Defaults to 'y'.

API Reference

Signature

useScroll(options?: UseScrollOptions): {
  scrollX: AugmentedMotionValue<number>
  scrollY: AugmentedMotionValue<number>
  scrollXProgress: AugmentedMotionValue<number>
  scrollYProgress: AugmentedMotionValue<number>
}
useScroll(options?: UseScrollOptions): {
  scrollX: AugmentedMotionValue<number>
  scrollY: AugmentedMotionValue<number>
  scrollXProgress: AugmentedMotionValue<number>
  scrollYProgress: AugmentedMotionValue<number>
}

Returns

An object with four AugmentedMotionValue<number>s — real motion-dom MotionValues with .current getter, .subscribe shim, and all standard motion-value methods (get, getVelocity, on, etc.).

See also


Based on Motion’s useScroll API.