Scrollwork · v1.15.1 · MIT

Motion from one attribute.

This heading: data-scrollwork='{"appear": {"effect": "mask", "split": "lines", "stagger": 0.1, "duration": 1}}'

The open-source motion engine behind every site published from uxdeck. Appear, scroll, pin, loops, rollovers and WebGL effects, written as data in your HTML or called from code. Free for everyone.

npm install scrollwork
  • 29 KB core, gzipped
  • 0 dependencies
  • +10 KB effects, optional
  • +5 KB extras, optional

Playground

Edit it, see it.

Three elements of a small page, each with its attribute open. Change a value and it plays at once, with the same engine you would install. If something is wrong, Scrollwork says what.

Edit any value: it plays as you type.

Your page

Write it once, watch it move

Get started

Every motion

Everything it plays.

Each tile is the code under it, running. Copy one into your page and it does the same there.

Appear

Plays once as the element comes into view. Text can split into lines, words or letters.

Motion that tells the story

Mask, by line

Each line rises out of its own box.

<p data-scrollwork='{"appear": {"effect": "mask", "split": "lines"}}'>Motion that tells the story</p>

Motion that tells the story

Letter by letter

The same mask, one letter at a time.

<p data-scrollwork='{"appear": {"effect": "mask", "split": "chars", "stagger": 0.025}}'>Motion that tells the story</p>

Motion that tells the story

Slide up, by word

Words rise and fade in, one after another.

<p data-scrollwork='{"appear": {"effect": "slide-up", "split": "words", "stagger": 0.06}}'>Motion that tells the story</p>

Motion that tells the story

Blur in

Out of focus to sharp.

<p data-scrollwork='{"appear": {"effect": "blur", "split": "words", "stagger": 0.05}}'>Motion that tells the story</p>

Scale in

A picture grows into place.

<div data-scrollwork='{"appear": {"effect": "scale", "duration": 1.1}}'></div>

1,280

Rolling digits

Each digit turns like an odometer.

<p data-scrollwork='{"appear": {"effect": "roll", "turns": 2, "duration": 1.6}}'>1,280</p>

Your own start, with steps

A custom start state and a step on the way.

<div data-scrollwork='{"appear": {"effect": "custom", "from": {"y": 80, "rotate": -10, "opacity": 0}, "keys": [{"at": 60, "state": {"y": -12, "rotate": 3}}], "duration": 1}}'></div>

Motion that tells the story

Words that flash

Each word arrives in gold, then takes its own colour.

<p data-scrollwork='{"appear": {"effect": "custom", "split": "words", "from": {"opacity": 0, "ink": "#F2C14E"}, "keys": [{"at": 57, "state": {"ink": "#F2C14E"}}], "duration": 0.35, "stagger": 0.1, "ease": "out"}}'>Motion that tells the story</p>

Scroll

Follows the scroll position: parallax, or any state scrubbed between two points.

Parallax

Scroll the page: the picture drifts inside its frame.

<div><div data-scrollwork='{"scroll": {"speed": 40, "cover": true}}'></div></div>

Scrubbed state

Turns and grows as it crosses the screen.

<div data-scrollwork='{"scroll": {"from": {"rotate": -20, "scale": 0.7}, "to": {"rotate": 20, "scale": 1.05}}}'></div>

Tipped in 3D

Leans back, then stands up as it reaches the middle.

<div data-scrollwork='{"scroll": {"from": {"rotateX": 55, "opacity": 0.3, "y": 60}, "to": {}, "range": "in"}}'></div>

Reveal from a corner

A clip opens from the top right as it scrolls in.

<div data-scrollwork='{"scroll": {"from": {"clip": {"top": 100, "right": 100}}, "to": {}, "range": "in"}}'></div>

Motion should follow the reader, not the clock. Here every word waits for the scroll, so the paragraph is read at the pace of the hand on the wheel.

Light up as read

Words brighten as the paragraph moves up.

<p data-scrollwork='{"scroll": {"split": "words", "spread": 0.3, "from": {"opacity": 0.15}, "to": {}, "range": "in"}}'>Motion should follow the reader, not the clock. Here every word waits for the scroll, so the paragraph is read at the pace of the hand on the wheel.</p>

Scrollwork

Slices

One line cut into slices that come together.

<p data-scrollwork='{"scroll": {"split": "slices", "slices": 6, "order": "center", "spread": 0.7, "from": {"x": -140, "scaleX": 1.5, "scaleY": 0}, "to": {}, "range": "in", "ease": "ease-in-out-quad"}}'>Scrollwork</p>
Gold

Colour on scroll

The background moves to gold and the words to black.

<div data-scrollwork='{"scroll": {"from": {}, "to": {"fill": "#F2C14E", "ink": "#0B0B0D"}, "range": "in"}}'>Gold</div>

A formation

Every child flies into the grid from the centre out.

<div data-scrollwork='{"scroll": {"split": "children", "order": "center", "outward": true, "spread": 0.4, "from": {"y": 220, "rotate": 15, "opacity": 0}, "to": {}, "range": "in", "ease": "ease-out-cubic"}}'><div></div>…</div>

Loops

Plays over and over while it is on screen, and rests when it is not.

AppearScrollPinLoopHoverCursorLoaderEffectsAppearScrollPinLoopHoverCursorLoaderEffects

Marquee

Two copies of a row, moved by one; the edges fade.

<div><p data-scrollwork='{"loop": {"to": {"x": -640}, "duration": 16, "ease": "linear", "fade": 60}}'><span>Appear</span>…</p></div>

Appear

Scroll

Pin

Loop

Appear

Scroll

Pin

Loop

Marquee up

The same, upwards.

<div><div data-scrollwork='{"loop": {"to": {"y": -240}, "duration": 10, "ease": "linear", "fade": 48}}'><p>Appear</p>…</div></div>

A ring, kept upright

The ring turns; every picture stays level.

<div data-scrollwork='{"loop": {"to": {"rotate": 360}, "duration": 24, "ease": "linear", "upright": true}}'><div></div>…</div>

Flipbook

The children shown one at a time.

<div data-scrollwork='{"loop": {"flipbook": 0.35, "duration": 4}}'><div></div>…</div>

Float

There and back, on a gentle curve.

<div data-scrollwork='{"loop": {"to": {"y": -16, "rotate": -2}, "duration": 2.6, "ease": "ease-in-out-sine", "yoyo": true}}'></div>

Spin

One slow turn after another.

<div data-scrollwork='{"loop": {"to": {"rotate": 360}, "duration": 8, "ease": "linear"}}'></div>

Hover, press and rollovers

States to move to under the pointer, with keyboard focus doing the same.

Hover and press

Point at the card, then press it.

<button data-scrollwork='{"hover": {"y": -8, "scale": 1.04}, "press": {"scale": 0.96}}'>Point at me</button>

Colour under the pointer

The card and its words change colour.

<button data-scrollwork='{"hover": {"fill": "#F2C14E", "ink": "#0B0B0D"}}'>Point at me</button>

Letter roll

The label rolls out as a copy rolls in.

<a href="/start"><span data-scrollwork='{"rollover": {"effect": "roll", "direction": "up", "stagger": 0.02}}'>Get started</span></a>

Fill wipe

A colour sweeps across from the left.

<a href="/start" data-scrollwork='{"rollover": {"effect": "wipe", "direction": "right", "color": "#F2C14E", "ink": "#0B0B0D"}}'>Get started</a>

Scramble

Letters flicker through random characters before they land.

<a href="/start" data-scrollwork='{"rollover": {"effect": "scramble", "on": "both", "cursor": true, "colors": ["#F2C14E"]}}'>Read the docs</a>

Interactions

Click it: a change on a spring, and back on the next click.

<button data-scrollwork='{"interactions": [{"trigger": "click", "action": {"type": "change", "state": {"rotate": 180, "scale": 0.9}}, "animation": {"curve": "spring", "spring": [600, 15, 1], "duration": 0.9}}]}'>Click me</button>

Cursor and loader

The page’s own pointer, and a screen that plays first.

A cursor of your own

Move the pointer in this box: it is followed, and grows over the button.

<div><div data-scrollwork='{"cursor": {"lag": 0.15, "links": {"scale": 3}}}'></div><a href="/start">A link</a></div>

100

Loader

Covers the page as it opens, rolls its number, then leaves; everything else waits.

<div data-scrollwork='{"loader": {"hold": 1.6, "leave": 0.8, "effect": "blur"}}'><div data-scrollwork='{"loop": {"flipbook": 0.3, "duration": 4}}'><div></div>…</div><p data-scrollwork='{"appear": {"effect": "roll", "duration": 1.4}}'>100</p></div>

Effects

Shader effects on a picture or on words, from an optional file. Without WebGL2 or under reduced motion, the picture shows as it is.

Glitchfx

Point at the picture.

<div data-scrollwork='{"fx": [{"type": "glitch"}]}'></div>

Pixel distortionfx

Move the pointer across it; the cells ease back.

<div data-scrollwork='{"fx": [{"type": "distort", "grid": 40, "strength": 0.6}]}'></div>

Halftonefx

A field of dots; the pointer leaves a trail.

<div data-scrollwork='{"fx": [{"type": "dither", "mode": "halftone", "size": 5, "color": "#EDEDEF", "color2": "transparent", "accent": "#F2C14E"}]}'></div>

Bulge

Bulgefx

Point at the word: it swells towards you.

<p data-scrollwork='{"fx": [{"type": "bulge", "reach": 0.25}]}'>Bulge</p>

Claims you can check

Built to stay out of the way.

Three promises from the README, each one working in front of you. The steps themselves are a pin and a scroll on its hold, written as attributes.

01

It leaves your styles alone.

The card is centred and tilted by its own CSS: left: 50%; translate: -50% 0; rotate: -6deg. Scrollwork floats it up and down on top of that, so it stays on the centre line and keeps its tilt.

02

It respects the reader.

With reduce motion on, everything appears at rest and nothing drifts, while delays are kept. If the setting changes during a visit, Scrollwork follows it. Here is the same motion played twice.

As most people see it
With reduce motion on

03

It rests when nothing moves.

No animation frame runs while the page is still. Scroll a little, then stop: the count below is this page asking for frames, measured live. It drops to 0 a moment after you stop.

The number is frames asked for in the last second; the bars, the last 30 seconds.

From code

Or write it in code.

animate(), timeline(), inView() and scroll(), with TypeScript types. Values are relative to the element's own look, and any CSS property, variable or colour can move.

scroll() Follow the page

The gold bar above is this section's passage across your screen: scrubbing an animation is two lines.

import { animate, scroll } from 'scrollwork';

const fill = animate('.progress', { scaleX: [0, 1] }, { autoplay: false });
scroll(fill, { target: '.section' });

animate() Move anything

import { animate } from 'scrollwork';

animate('.box', { y: [60, 0], rotate: [-12, 0], opacity: [0, 1] }, {
  duration: 0.8, ease: 'expo', stagger: 0.08,
});

timeline() In order

import { timeline } from 'scrollwork';

const tl = timeline({ defaults: { duration: 0.6, ease: 'out' } })
  .add('.bar', { scaleX: [0, 1] })
  .add('.dot', { scale: [0, 1] }, {}, '-=0.2')
  .add('.label', { y: [20, 0], opacity: [0, 1] });

slider.oninput = () => tl.seek(slider.value / 100);

Curves and springs

Nine named curves, the thirty of easings.net, any cubic bezier, a spring, or a function of your own. Pick one to see it drawn and played.

expo, 1 s

import { animate, spring, SPRINGS } from 'scrollwork';

animate(ball, { x: 280 }, { ease: 'expo', duration: 1 });

// a spring is a curve and the time it takes to settle
const { ease, duration } = spring(...SPRINGS.bouncy);
animate(ball, { x: 280 }, { ease, duration });

inView() On arrival, and back

These count up as they arrive and back down as they leave. Scroll away and come back.

29KB core, gzipped

0dependencies

3browser engines, every release

import { animate, inView } from 'scrollwork';

inView('.stat', (el) => {
  const n = { value: 0 };
  const a = animate(n, { value: Number(el.dataset.to) }, {
    duration: 1.4, ease: 'out',
    onUpdate: () => (el.textContent = Math.round(n.value)),
  });
  return () => a.reverse(); // runs as it leaves
});

scrollwork/extras · +5 KB

Extras, when you need them.

Flip, morph, follow, drag and sideways, in a file of their own that only the pages using them load.

flip() Layout changes, animated

const before = capture('.card');
grid.classList.toggle('is-list');
flip(before, { duration: 0.6, ease: 'expo', stagger: 0.03 });

morph() One shape into another

morph('#shape', '#star', { duration: 0.9, ease: 'in-out' });

follow() Along a path, turning with it

Scroll the page: the mark travels the line.

const mark = follow('.mark', '#route', { rotate: true, align: true });
scroll(animate(mark, { progress: [0, 1] }, { autoplay: false }), { target: '.card' });

drag() Thrown, caught on a snap point

Drag the belt and let go, or focus it and use the arrow keys.

drag('.belt', { axis: 'x', bounds: '.rail', snap: 196 });

sideways() A row that moves across while you scroll down

sideways('.panels', { snap: 'panels', smooth: 0.2 });

01

Appear

Once, on arrival.

02

Scroll

Tied to the wheel.

03

Pin

Held while you scroll.

04

Loop

Round and round.

05

Effects

Pictures that answer.

Made with Scrollwork

Running on real pages.

Install

On your page in a minute.

1. Add it

From npm, with TypeScript types:

npm install scrollwork

Or as a plain script from a CDN, or the file from the latest release:

<script src="https://cdn.jsdelivr.net/npm/scrollwork@1/dist/scrollwork.min.js" data-auto></script>

2. Describe the motion

With data-auto, it plays every attribute once the page has loaded:

<h1 data-scrollwork='{"appear": {"effect": "mask", "split": "lines"}}'>
  Motion that tells the story
</h1>

<img src="hero.jpg" alt="" data-scrollwork='{"scroll": {"speed": 30}}'>

<script src="scrollwork.min.js" data-auto></script>

3. Avoid a flash

Two lines in the head hide only what is about to appear, and show it anyway after three seconds if the script never loads:

<style>html.sw-pending [data-scrollwork*='"appear"'] { opacity: 0 }</style>
<script>document.documentElement.classList.add('sw-pending');
setTimeout(() => document.documentElement.classList.remove('sw-pending'), 3000);</script>

Every field and its default is in the reference, and the whole API in the README.

For everyone who reads the page

Accessible by default.

  • Reduced motion. Elements appear at rest, parallax and scrubbing stop, hover snaps, smooth scrolling is off. It follows the setting when it changes during a visit.
  • Keyboard. Hover states show on focus; press states answer Enter and Space.
  • Screen readers. Split text is read once, whole. A rolling label is read once, without its copy.
  • Nothing goes blank. Without WebGL2, an effect shows its picture as it is. Broken JSON uses the defaults and says why in the console.
  • Browsers. Every release is tested in current Chromium, WebKit and Firefox. Built for Chrome 104, Safari 14.1 and Firefox 72 or later.
  • Your design system. Durations and curves can come from CSS variables, so motion follows your tokens.
  • Credits. The distort and bulge effects, and sliced text, follow demos by Codrops (MIT); their notices ship with the package.
  • Licence. MIT, © 2026 uxspot.io. Use it in any project, commercial ones included.

Free, for everyone.

I built Scrollwork to run the sites people publish from uxdeck, then opened it to everyone. Use it in your own code, or draw the same motion in uxdeck without writing any.