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

@magic-spells/split-text

v0.1.2

Published

Lightweight text-reveal web component — slide-up animation by word, character, or line with no dependencies

Readme

Split Text Web Component

Lightweight text-reveal web component. Splits text into words, characters, or detected lines, then animates each unit with a CSS-driven stagger. Eight built-in effects — rise, drop, slide, bloom, two 3D spins, and a per-character magnetic scatter. No dependencies. No framework. ~2.7 KB gzipped (JS) + 0.9 KB (CSS).

A small alternative to GSAP SplitText / Splitting.js when you don't need the rest of those libraries.

Live Demo

Features

  • Three split modes: words, chars, lines (auto-detected from layout)
  • Eight effects: rise (default), drop, slide-right, slide-left, bloom, spin-x, spin-y, magnetic
  • CSS-only animation — transform, opacity, and filter, GPU-accelerated
  • Triggers when scrolled into view by default (configurable: load, manual)
  • Preserves inline markup (<em>, <strong>, <a>, <br>) inside the host
  • Emoji-safe character splitting via Intl.Segmenter
  • Honors prefers-reduced-motion
  • After animation, wrappers swap to display: contents so text reflows naturally on resize
  • Self-registers; safe to import multiple times

Installation

npm install @magic-spells/split-text
import '@magic-spells/split-text';

Or include directly:

<script src="https://unpkg.com/@magic-spells/split-text"></script>
<link rel="stylesheet" href="https://unpkg.com/@magic-spells/split-text/css/min" />

Usage

<split-text>The quick brown fox jumps over the lazy dog.</split-text>

That's it. The element splits on words by default and animates as soon as it scrolls into view.

Modes

<!-- Words (default) -->
<split-text split="words">Each word slides up independently</split-text>

<!-- Characters -->
<split-text split="chars">Letter by letter</split-text>

<!-- Lines (auto-detected from layout) -->
<split-text split="lines">
  This longer paragraph wraps onto multiple lines, and each line
  animates as a unit. Resize the window before scrolling to it and
  the line groups recalculate to match the new layout.
</split-text>

Effects

<!-- rise (default) — slides up from below -->
<split-text>Rises into view.</split-text>

<!-- drop — falls in from above -->
<split-text effect="drop">Falls into place.</split-text>

<!-- slide-right / slide-left — sweeps in horizontally -->
<split-text effect="slide-right" split="chars">Sliding in from the right.</split-text>
<split-text effect="slide-left" split="chars">And from the left.</split-text>

<!-- bloom — blur + scale pop, no translate -->
<split-text effect="bloom" split="chars">Comes into focus.</split-text>

<!-- spin-x — rotateX flip (origin: bottom by default) -->
<split-text effect="spin-x" split="chars">Stands up from baseline.</split-text>

<!-- spin-y — rotateY flip (origin: left by default) -->
<split-text effect="spin-y" split="chars">Swings open like a door.</split-text>

<!-- magnetic — per-character random scatter, snaps into place -->
<split-text effect="magnetic" split="chars">Pulled into place.</split-text>

The 3D spin effects rely on perspective: 2000px set on the host by default. Override with --split-text-perspective, and shift the pivot with --split-text-origin. The bloom effect's starting state is tunable via --split-text-blur and --split-text-scale.

The magnetic effect is built for split="chars". Each character starts shifted to the right by a random distance at a random height, blurred and transparent, then accelerates home on an ease-in curve (override with easing). The per-unit offsets are randomized in JS — replaying via .split() reshuffles them. Pieces are intentionally not clipped, so they travel outside their box; the starting blur is shared with bloom via --split-text-blur.

Timing

<split-text delay="200" stagger="50" duration="900">
  Wait 200ms, then 50ms between each word, 900ms per word.
</split-text>

Triggers

<!-- Default: animate when scrolled into view (waits until 100% visible) -->
<split-text trigger="visible">…</split-text>

<!-- Animate immediately on load -->
<split-text trigger="load">…</split-text>

<!-- Manual trigger -->
<split-text id="hero" trigger="manual">…</split-text>
<script>
  document.querySelector('#hero').reveal();
</script>

Adjusting the trigger position

offset shifts the trigger line up from the bottom of the viewport. The default is "20%" — text animates once the element has scrolled into the bottom fifth of the viewport, rather than the instant it peeks in.

<!-- Default — 20% of viewport height above the bottom -->
<split-text>…</split-text>

<!-- Custom: 200px above the bottom of the viewport -->
<split-text offset="200px">…</split-text>

<!-- Disable the offset — fire on first intersection -->
<split-text offset="0">…</split-text>

Custom Easing

<split-text easing="cubic-bezier(0.34, 1.56, 0.64, 1)">
  A spring overshoot for that playful feel.
</split-text>

Inline Markup

Inline tags inside the host are preserved. Links remain clickable, emphasis still styles, line breaks force new lines:

<split-text split="lines">
  Built with <em>care</em> and <a href="/about">a few good ideas</a>.<br>
  Try resizing the window before scrolling here.
</split-text>

Attributes

| Attribute | Default | Description | | ------------- | ------------------------------- | ---------------------------------------------------- | | split | words | words, chars, or lines | | effect | rise | rise, drop, slide-right, slide-left, bloom, spin-x, spin-y, magnetic | | delay | 0 | Initial delay before animation starts (ms) | | stagger | 30 | Delay between each unit (ms) | | duration | 800 | Animation duration per unit (ms) | | easing | cubic-bezier(0.16, 1, 0.3, 1) | CSS easing function | | trigger | visible | visible, load, or manual | | offset | 20% | Distance above bottom of viewport before firing (px or %; set 0 to disable) |

CSS Custom Properties

Override these for theming. They're already wired through the CSS — set them on the host or any ancestor.

| Property | Default | Description | | --------------------------- | ------------------------------- | ---------------------------------------------------------------------------- | | --split-text-duration | 800ms | Animation duration | | --split-text-easing | cubic-bezier(0.16, 1, 0.3, 1) | Animation easing | | --split-text-distance | 100% | Travel distance for rise / drop / slide-* (try 40% for subtler) | | --split-text-stagger | 30ms | Delay between units | | --split-text-delay | 0ms | Initial delay | | --split-text-perspective | 2000px | 3D camera distance — applies to spin-x / spin-y | | --split-text-origin | per-effect | transform-origin for spin (defaults: bottom for spin-x, left for spin-y) | | --split-text-blur | 2px | Starting blur radius (bloom & magnetic) | | --split-text-scale | 0.7 | Bloom starting scale |

split-text {
  --split-text-easing: ease-out;
  --split-text-distance: 60%;
}

Methods

const el = document.querySelector('split-text');

el.reveal();  // trigger animation manually (when trigger="manual")
el.split();   // reset and re-split (call after changing innerHTML)

Events

Both events bubble.

el.addEventListener('split-text:start', (e) => {
  console.log(e.detail); // { split: "words", count: 12 }
});

el.addEventListener('split-text:complete', (e) => {
  console.log('done', e.detail);
});

How it works

The script walks the host's text nodes, wraps each word (or grapheme, in chars mode) in two nested spans — an outer mask with overflow: hidden and an inner element that animates from a pre-reveal state to its final position. Each inner span gets a --i index; CSS computes animation-delay from it. The effect attribute selects which keyframe runs — duration, easing, and stagger are shared across every effect. (The magnetic effect is the one exception to the overflow: hidden mask: its pieces fly in from outside the box, so the wrapper is set to overflow: visible, and JS stamps each inner span with a random --split-text-mx / --split-text-my start offset.)

For lines mode, the script splits into words first, measures each word's getBoundingClientRect().top after layout, groups consecutive words by their top position (with tolerance for sub-pixel rendering), and assigns every word in a line the same --i. Words are never re-parented, so any inline markup inside (links, emphasis, etc.) survives the split.

Once animation completes, the host gets data-revealed and CSS sets every internal wrapper to display: contents. Layout becomes identical to the original markup — text reflows naturally on resize, with no JS work and no innerHTML thrash.

Accessibility

  • The host's aria-label is set to the original plain text, so screen readers announce the sentence once instead of wrapper-by-wrapper.
  • Generated word wrappers are marked aria-hidden="true" to prevent the AT from re-reading the same text the aria-label already covers.
  • Per the ARIA spec, aria-hidden is skipped on wrappers inside interactive ancestors (<a>, <button>, <summary>, form controls, [role="button"], focusable [tabindex], etc.). Nested links and buttons retain their accessible names.
  • prefers-reduced-motion: reduce disables the animation entirely; content shows immediately.

Browser Support

Modern browsers with custom elements + IntersectionObserver support (Chrome, Firefox, Safari, Edge). Intl.Segmenter is used when available for grapheme-correct character splitting; older Safari falls back to spread iterator (handles emoji correctly, just not full grapheme clusters).

License

MIT