npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

words-in-motion

v0.1.3

Published

Typographic motion for the web.

Readme

words-in-motion

Typographic motion for the web.

A lightweight collection of standalone, ready-to-use typographic animations, scroll-driven effects, and cursor-reactive interactions for the web built natively on the Web Animations API (WAAPI) and inline transforms. Zero dependencies. Tree-shakeable. Accessible.


Features

  • ⚡ Zero Runtime Dependencies — Pure JavaScript & WAAPI. No GSAP or heavy animation libraries.
  • 🌳 Tree-Shakeable Subpath Exports — Import only the specific categories or animations you use (words-in-motion/intro, words-in-motion/loop, words-in-motion/outro, words-in-motion/interact, words-in-motion/scroll).
  • 📜 Scroll-Driven Motion — Viewport trigger mode (plays on enter) and scrub mode (ties character progress directly to scroll position).
  • 🖱️ Cursor-Reactive Interact Effects — Long-running interactive text effects with built-in pointer tracking and destroy() cleanup.
  • ♿ Accessibility Built-In — Automatic aria-label preservation on split containers and support for prefers-reduced-motion.
  • ⚡ SSR & Framework Friendly — Safe for Next.js, Nuxt, Remix, and SvelteKit. No DOM access at import time.
  • 🔤 Emoji & RTL Safe — Grapheme-level text splitting via Intl.Segmenter with automatic Arabic/cursive script fallback.

Installation

npm install words-in-motion

Quick Start

import { pixelResolve } from 'words-in-motion/intro';

// Animate an element by CSS selector or HTMLElement reference
const handle = pixelResolve('#title', {
  pixelSize: 16,
  steps: 8,
  duration: 1600,
});

// Await completion or cancel early
await handle.finished;
// handle.cancel();

Animation Categories

Intro Animations (words-in-motion/intro)

Available: directionalReveal, lampFlicker, rise, chunkedScramble, stripRealign, and pixelResolve.


Loop Animations (words-in-motion/loop)

| Effect | Motion | Default cycle | | ---------- | ----------------------------- | ------------- | | wave | Traveling vertical wave | 2400ms | | float | Gentle elliptical drift | 4200ms | | breathe | Word-level expansion and rest | 3600ms | | shimmer | Subtle opacity sweep | 2800ms | | pendulum | Phased letter tilts | 3000ms |

import { wave } from 'words-in-motion/loop';

const loop = wave('.headline', { duration: 2400, intensity: 1, by: 'chars' });
// In your component's unmount / effect cleanup:
loop.cancel();
await loop.finished;

Loops repeat until canceled. finished resolves on cancellation, reduced motion, or a missing/empty target. duration is one cycle; delay is the initial delay; stagger is the phase delay between units. intensity ranges from 0 to 4. by: 'words' preserves whole words. Hidden browser tabs pause their WAAPI loops. A reduced-motion preference change cancels active loops and restores the text.


Outro Animations (words-in-motion/outro)

Available: blackHole (spiral collapse), paperCut (alternating slices), breakAndFade (reading-order fracture), hingeDrop (pivot and fall), blurAway (soft defocus), and windScatter (directional gust). All support wrapped text, delay, and keep: true to restore visibility after completion. By default, completion hides the target; cancel() restores its original opacity and content.


Interact Animations (words-in-motion/interact)

Available: pull, push, obstaclePush, proximityFade, proximityFlip, proximityRotate, proximityShake, fontWeight, and accentColor. Effects compose on the same target. pause() eases back to rest; resume() reactivates; destroy() restores original nodes and removes observers/listeners when the last effect is destroyed. Weight changes are smoothest with a variable font; color mixing uses native CSS color-mix().


Scroll Animations (words-in-motion/scroll)

Scroll animations support two distinct modes:

  • Trigger Mode (createScrollTrigger): Plays an animation when the element enters the viewport (e.g. start: "top 80%"). Can play once or repeat every time it enters the viewport (repeat: true). Returns { finished, cancel, destroy }.
  • Scrub Mode (createScrollScrub): Progress (0..1) is tied directly to the element's Y position as it scrolls through the viewport. Returns { destroy, pause, resume }.

Available scrub effects: readingLine (word-by-word opacity), scatterReassemble (staggered settling), and waveRelay (a traveling character following reading order). Positions refresh on resize and font loading. Endpoints settle exactly, including when scrolling rapidly past the target. These effects change only transforms, opacity, and filters during playback.


Accessibility & SSR Notes

prefers-reduced-motion

words-in-motion automatically checks browser and system settings for reduced motion preferences (prefers-reduced-motion: reduce). When enabled, intro/loop/outro animations complete instantly or shorten cleanly, interact effects disable displacement, and scroll animations complete cleanly.

Touch Devices & Cursor Interactions

Interact effects support configurable touch behavior (touch: 'follow' | 'tap' | 'none'). On touch devices, interactions react gracefully to touch events or taps without interfering with page scrolling.

Screen Readers (aria-label & aria-hidden)

When splitting text into character or word elements, words-in-motion preserves original text inside an aria-label attribute on the container element while applying aria-hidden="true" to generated span nodes.

Server-Side Rendering (SSR)

All exports are guaranteed free of module-level side effects or immediate window/document property access. You can safely import words-in-motion in Next.js Server Components, Nuxt, or Node.js environments.


License

MIT © 2026 Patrick Marcus

Pixel resolution and integration notes

import { pixelResolve } from 'words-in-motion/intro';
const intro = pixelResolve('.headline', { pixelSize: 16, steps: 8, duration: 1600 });
await intro.finished;

pixelResolve samples the rendered words into a low-resolution canvas and increases the grid density geometrically until the original text returns. The default block size follows the font size. Original text remains in the DOM for accessibility. Nested colors, explicit line breaks, alignment, RTL words and letter spacing use browser measurements. Resize or font loading ends the effect early to keep text readable. Call it after your web fonts load for the best visual match.

Canvas text is an approximation of browser typography: text shadows, gradient fills, decorations and advanced font features are not rasterized. Vertical writing, transformed hosts, zero-size targets, unavailable canvas, and targets larger than four million CSS pixels retain their original text. Apply it to a text wrapper rather than a container containing unrelated visual content.

The updated splitter preserves original nodes (including event listeners), inline markup, explicit breaks, emoji graphemes, and joined-script word shaping. Avoid changing the contents while an effect is running; cancel/destroy first, update your text, then restart. Extremely long unbroken tokens retain word-level wrapping. Always clean up handles when a component unmounts. Starting a loop, outro, scrub, or pixel resolution on a target replaces the previous effect that owns its text; multiple cursor effects can compose together.

Motion references

Original implementations, informed by these motion studies; no external animation code or runtime libraries are included:

The new defaults favor small displacement, smooth boundaries and readable resting states. They are intended as adaptable building blocks, not replicas of those sites.

Solid letter interactions

pull and push are soft cursor fields: nearby letters move smoothly and may overlap, with the cursor never colliding with them. obstaclePush is the separate contact-based effect that physically moves letters out of the cursor's way. Collision checks use measured text rectangles, including word-sized bodies for joined scripts. They preserve existing tracking at rest; this is not pixel-perfect glyph-outline physics. Contacts work within each target, including between its wrapped lines.

import { obstaclePush, proximityRotate } from 'words-in-motion/interact';

// Only the cursor tip touching a letter moves it; there is no surrounding field.
const contact = obstaclePush('.title', { easing: 0.15 });
// A quarter-turn by default; use maxAngle to choose a different range.
const rotation = proximityRotate('.other-title', { maxAngle: 90 });

// On unmount:
contact.destroy();
rotation.destroy();

Letters return to their measured home positions when the cursor leaves or the effect is paused. Fast movement is subdivided before resolving contacts. pull and push retain their radius and pixel strength controls; obstaclePush uses cursorRadius: 0 by default for contact at the cursor tip. A larger physical contact area is opt-in through cursorRadius; radius does not create a force field. Allow enough surrounding space (or visible overflow) for the displaced letters.