English · Türkçe
A scroll-driven interaction runtime. Vanilla JS, zero dependencies, and it generates no DOM.
You author real, semantic HTML and mark it up with data-sc-* attributes. The
engine reads them and drives everything from a single scroll value on a single
requestAnimationFrame loop.
That constraint is the point. A runtime that builds its own DOM from a config object makes every site it touches look identical — which is the failure mode this replaces. You get a mechanism and a vocabulary; the composition stays yours.
<link rel="stylesheet" href="scrollcraft.css">
<section data-sc-act="pin" data-sc-span="2">
<div class="sc-stage">
<h2 data-sc-kinetic="words" data-sc-cue="0.05 0.75">Words arrive as you scroll</h2>
</div>
</section>
<script src="scrollcraft.js"></script>
<script>ScrollCraft.mount(document.body)</script>mount(root, opts) takes an element or a selector, and returns nothing you have
to keep. ScrollCraft.reduce reports whether reduced motion is on;
ScrollCraft.instances holds what has been mounted.
A running example is in example/ — open example/index.html.
An act owns a stretch of scroll and exposes a normalized progress p (0..1),
published as --sc-p on the element so CSS can read it too.
<section data-sc-act="pin" data-sc-span="2.5"> … </section>data-sc-act |
scrub · pin · pan · flow (default flow) |
data-sc-span |
viewport-heights of scroll this act owns. Pinned acts only. Default 1.5 |
The engine sets the outer height and sticks the first .sc-stage child.
| Attribute | On | What it does |
|---|---|---|
data-sc-scrub |
<video> |
p scrubs currentTime. Blob-loaded, so seeking needs no HTTP range support |
data-sc-sequence="a/{i}.webp:120:1" |
<canvas> |
p scrubs an image sequence — path template : frame count : start index |
data-sc-pan="0.6" |
a wide rail | horizontal travel; the value is an extra-travel multiplier |
data-sc-parallax="-0.2" |
any | translateY by rate × progress × viewport. Negative recedes |
data-sc-cue="0.1 0.5" |
any | opacity and rise keyed to p. One value = enter+hold, two = enter..leave |
data-sc-kinetic="lines" |
text | lines · words · chars. Splits and staggers across the cue window |
data-sc-reveal="up" |
any | up · down · left · right · iris clip-path wipe |
data-sc-count="0 4200" |
any | number bloom across the cue window |
data-sc-in |
a flow section | reveal that fires once on entry. data-sc-stagger="60" staggers children |
data-sc-drift="#07090c" |
an act | page background interpolates toward this colour while the act is on screen |
data-sc-progress |
any | the engine fills it as the page scrolls; it draws no chrome of its own |
Acts cut a page into pinned blocks, which is right for chapters and wrong for a single unbroken flight: the reader hits the end of an act, the stage unsticks, a static page slides past, and the next act starts the whole thing again.
Worldflight removes the seams by removing the blocks. There is one fixed
stage for the whole page. The only element in flow is a spacer whose height the
engine sets from the sum of segment weights. Every clip stays mounted for the
life of the page and segments crossfade over a seam band — nothing ever swaps a
src, because a src swap is a black frame, and a black frame is the cut this
mode exists to avoid.
data-sc-w |
viewport-heights this segment owns. Default 1.3 |
data-sc-linger |
dwell remap for this leg only. Max 0.6 |
data-sc-seam |
crossfade band, in viewport-heights. Default 0.12 |
data-sc-waypoint |
label published on --sc-seg and the sc:waypoint event, so a page can draw its own route rail. The engine draws none |
data-sc-window |
on a copy block: hero · finale · from to [in [out]] as fractions of the whole track |
data-sc-tilt="8" |
3D tilt toward the pointer, spring-damped, in degrees |
data-sc-magnet="0.35" |
element drifts toward the pointer inside its bounds |
data-sc-spotlight |
publishes --sc-mx / --sc-my (0..1) for a light that follows the pointer |
All three are gated to (hover: hover) and (pointer: fine) and switched off
under reduced motion. Touch never fires them.
Fewer and gentler, not zero. Cues still fade, so comprehension survives. Translation collapses, video clips are never fetched at all (the poster holds), and pointer devices are inert. A worldflight still tells its whole story: the posters cross-dissolve through the same seams and the same copy windows.
A rail is the exception that proves the rule. Zeroing its transform the way parallax is zeroed would park the act on its first screenful and make everything past the fold unreachable — a device that degrades into inaccessible content is not an accessibility win.
Every scrub clip on the page — act or worldflight — is driven by one smoothed
playhead. Scroll only ever writes a target; a standalone loop walks the
current time toward it at a fixed fraction per frame (data-sc-lerp, default
0.18; 1.0 under reduced motion, which is no smoothing).
Without the smoothing a trackpad flick reads as a stutter, because wheel events do not arrive at a constant rate and a 1:1 write reproduces every gap in them.
Two families, and mixing them up bites:
| Class | Where it belongs |
|---|---|
.sc-body · .sc-lede · .sc-display · .sc-label |
flowing text. .sc-body caps line length at the reading measure — it is a paragraph class, not a page class. Putting it on <body> locks the whole document to about 700px |
.sc-copy · .sc-copy--lead · .sc-copy--center · .sc-copy--trail |
overlay on a stage. These are position: absolute and anchor to the nearest positioned ancestor. Used in a flow section they escape to the top of the page |
.sc-stage is position: sticky, so it is the containing block a .sc-copy
inside it anchors to. That pairing is the intended one.
scrollcraft.css is two layers, and the split matters:
- Tokens — what you override per brand. Colour roles, the type ramp, the spacing scale, elevation, motion. A brand is about twelve values; override six colour roles and the page is rebranded.
- Devices — what the engine drives. Don't restyle these to taste; they are the mechanism. Style your own markup instead.
There are no card, badge or pill classes, because a stylesheet that ships those is how every page built on it ends up with the same shapes.
The stylesheet is required — the devices depend on it. It reads the document
dir, so a rail in an RTL page pans the other way without a second attribute.
MIT — LICENSE. Use it, change it, sell it.
Built for and running in production on hanzala.com.tr.