Motion that tells the story
Motion that tells the story
<p data-scrollwork='{"appear": {"effect": "mask", "split": "lines"}}'>Motion that tells the story</p>Explore uxdeck
Build with uxdeck
See it live
How to use uxdeck
Recent files
Your account
Scrollwork · v1.15.1 · MIT
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 scrollworkPlayground
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.
Every motion
Each tile is the code under it, running. Copy one into your page and it does the same there.
Plays once as the element comes into view. Text can split into lines, words or letters.
Motion that tells the story
Motion that tells the story
<p data-scrollwork='{"appear": {"effect": "mask", "split": "lines"}}'>Motion that tells the story</p>Motion that tells the story
Motion that tells the story
<p data-scrollwork='{"appear": {"effect": "mask", "split": "chars", "stagger": 0.025}}'>Motion that tells the story</p>Motion that tells the story
Motion that tells the story
<p data-scrollwork='{"appear": {"effect": "slide-up", "split": "words", "stagger": 0.06}}'>Motion that tells the story</p>Motion that tells the story
Motion that tells the story
<p data-scrollwork='{"appear": {"effect": "blur", "split": "words", "stagger": 0.05}}'>Motion that tells the story</p><div data-scrollwork='{"appear": {"effect": "scale", "duration": 1.1}}'></div>1,280
1,280
<p data-scrollwork='{"appear": {"effect": "roll", "turns": 2, "duration": 1.6}}'>1,280</p><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
Motion that tells the story
<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>Follows the scroll position: parallax, or any state scrubbed between two points.
<div><div data-scrollwork='{"scroll": {"speed": 40, "cover": true}}'></div></div><div data-scrollwork='{"scroll": {"from": {"rotate": -20, "scale": 0.7}, "to": {"rotate": 20, "scale": 1.05}}}'></div><div data-scrollwork='{"scroll": {"from": {"rotateX": 55, "opacity": 0.3, "y": 60}, "to": {}, "range": "in"}}'></div><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.
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 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
Scrollwork
<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><div data-scrollwork='{"scroll": {"from": {}, "to": {"fill": "#F2C14E", "ink": "#0B0B0D"}, "range": "in"}}'>Gold</div><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>Plays over and over while it is on screen, and rests when it is not.
AppearScrollPinLoopHoverCursorLoaderEffectsAppearScrollPinLoopHoverCursorLoaderEffects
AppearScrollPinLoopHoverCursorLoaderEffectsAppearScrollPinLoopHoverCursorLoaderEffects
<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
Appear
Scroll
Pin
Loop
Appear
Scroll
Pin
Loop
<div><div data-scrollwork='{"loop": {"to": {"y": -240}, "duration": 10, "ease": "linear", "fade": 48}}'><p>Appear</p>…</div></div><div data-scrollwork='{"loop": {"to": {"rotate": 360}, "duration": 24, "ease": "linear", "upright": true}}'><div></div>…</div><div data-scrollwork='{"loop": {"flipbook": 0.35, "duration": 4}}'><div></div>…</div><div data-scrollwork='{"loop": {"to": {"y": -16, "rotate": -2}, "duration": 2.6, "ease": "ease-in-out-sine", "yoyo": true}}'></div><div data-scrollwork='{"loop": {"to": {"rotate": 360}, "duration": 8, "ease": "linear"}}'></div>States to move to under the pointer, with keyboard focus doing the same.
<button data-scrollwork='{"hover": {"y": -8, "scale": 1.04}, "press": {"scale": 0.96}}'>Point at me</button><button data-scrollwork='{"hover": {"fill": "#F2C14E", "ink": "#0B0B0D"}}'>Point at me</button><a href="/start"><span data-scrollwork='{"rollover": {"effect": "roll", "direction": "up", "stagger": 0.02}}'>Get started</span></a><a href="/start" data-scrollwork='{"rollover": {"effect": "wipe", "direction": "right", "color": "#F2C14E", "ink": "#0B0B0D"}}'>Get started</a><a href="/start" data-scrollwork='{"rollover": {"effect": "scramble", "on": "both", "cursor": true, "colors": ["#F2C14E"]}}'>Read the docs</a><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>The page’s own pointer, and a screen that plays first.
<div><div data-scrollwork='{"cursor": {"lag": 0.15, "links": {"scale": 3}}}'></div><a href="/start">A link</a></div>100
100
<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>Shader effects on a picture or on words, from an optional file. Without WebGL2 or under reduced motion, the picture shows as it is.
<div data-scrollwork='{"fx": [{"type": "glitch"}]}'></div><div data-scrollwork='{"fx": [{"type": "distort", "grid": 40, "strength": 0.6}]}'></div><div data-scrollwork='{"fx": [{"type": "dither", "mode": "halftone", "size": 5, "color": "#EDEDEF", "color2": "transparent", "accent": "#F2C14E"}]}'></div>Bulge
Bulge
<p data-scrollwork='{"fx": [{"type": "bulge", "reach": 0.25}]}'>Bulge</p>Claims you can check
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
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
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.
Every word, in time
03
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
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 pageThe 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 anythingimport { animate } from 'scrollwork';
animate('.box', { y: [60, 0], rotate: [-12, 0], opacity: [0, 1] }, {
duration: 0.8, ease: 'expo', stagger: 0.08,
});timeline() In orderimport { 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);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 backThese 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
Flip, morph, follow, drag and sideways, in a file of their own that only the pages using them load.
flip() Layout changes, animatedconst before = capture('.card');
grid.classList.toggle('is-list');
flip(before, { duration: 0.6, ease: 'expo', stagger: 0.03 });morph() One shape into anothermorph('#shape', '#star', { duration: 0.9, ease: 'in-out' });follow() Along a path, turning with itScroll 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 pointDrag 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 downsideways('.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
Install
From npm, with TypeScript types:
npm install scrollworkOr 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>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>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
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.
A question, an idea, or need a hand? uxdeck is made by one designer, Soufiane. I read every message and answer by email.
Or write to contact@uxdeck.app