All Work
OPEN SOURCE / NPM SELF-INITIATED PRODUCT

scroll- craft.

The motion layer used in the client builds above, extracted into a zero-dependency scroll-animation library and published to npm: reveal, parallax, counters, text effects.

TypeScriptLibraryAccessibility
Role
Author
Timeline
2026-05-03 to 2026-05-05, 31 commits
Live URL
scroll-craft.bilalstudio.io ↗
scroll-craft demo page

01 / THE REQUEST

The motion layer,extracted.

The same reveal-on-scroll and parallax patterns kept getting rebuilt per client site. The fix was to write it once, correctly, and publish it, so every future build starts from a tested library instead of a copy-pasted IntersectionObserver.

02 / THE PROBLEM

Split text breaksscreen readers.

Character and word-level text reveal effects work by wrapping every letter in its own <span>. Done naively, that means a screen reader encounters a wall of single-letter nodes and spells the word out letter by letter instead of reading it. A library that does this for a living has to solve that, not ship it as a known issue.

03 / THE CHALLENGE

Observer gates,rAF animates.

The library's rule is IntersectionObserver decides whether to animate, requestAnimationFrame runs the animation, and ScrollTimeline is never used, since browser support for it still isn't reliable enough to build on. Parallax and scene effects split reading layout from writing it: a measure() pass and a write() pass are kept separate and coalesced into one rAF tick (src/effects.ts:1040-1124), so the browser never has to interleave a forced layout read with a style write and thrash.

Every effect feature-detects the APIs it needs and falls through to the final visual state if they're missing, so a browser without some capability shows the finished layout instead of invisible content stuck mid-animation.

The accessibility fix: the text splitter sets a synthetic aria-label carrying the original full string, marks every generated span aria-hidden="true", keeps whitespace as real text nodes rather than spans, and removes the synthetic label again on revert so the DOM returns to its original state (src/effects.ts:384-473). A screen reader reads the label, not the exploded spans.

04 / THE SOLUTION

What gotbuilt.

Observer-gated, rAF-driven

IntersectionObserver decides when to run; requestAnimationFrame runs it. No ScrollTimeline dependency.

Measure/write split

Layout reads and style writes are batched separately and coalesced into one rAF tick to avoid thrash.

Screen-reader-safe text splitting

A synthetic aria-label plus aria-hidden spans keep split-text effects legible to assistive tech.

Graceful feature detection

Every effect falls through to its final state when a required API is missing, rather than leaving content invisible.

05 / THE RESULT

On npm,in three days.

BUILT WITH

TypeScript (strict)tsupZero dependencies

Want the same motion system?

Next project

Licheanu