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

@liiift-studio/floodtext

v1.0.4

Published

Per-character variable font wave animation — weight, width, oblique, or opacity washes through the paragraph letter by letter

Readme

Flood Text

A wave washes through the paragraph character by character — modulating weight, width, oblique angle, or opacity as it passes. Not line by line, not word by word: every letterform sits at its own moment in the curve. At low amplitude it reads as texture; at high amplitude, as transformation.

floodtext.com · npm · GitHub

TypeScript · Zero dependencies · React + Vanilla JS


Install

npm install @liiift-studio/floodtext

Usage

Next.js App Router: this library uses browser APIs. Add "use client" to any component file that imports from it.

React component

import { FloodText } from '@liiift-studio/floodtext'

<FloodText effect="wght" amplitude={200} period={4} density={2} direction="diagonal-down">
  Your paragraph text here...
</FloodText>

Layer multiple effects simultaneously:

<FloodText effect={['wght', 'oblique']} period={4} density={2}>
  Your paragraph text here...
</FloodText>

React hook

import { useFloodText } from '@liiift-studio/floodtext'

// Inside a React component:
const ref = useFloodText({ effect: 'wght', amplitude: 200, period: 4, density: 2 })
return <p ref={ref}>{children}</p>

The hook starts the animation loop on mount, re-wraps characters and restarts on container resize via ResizeObserver, and cleans up on unmount.

Vanilla JS

applyFloodText wraps characters and returns them. startFloodText drives the animation loop and returns a stop function. Note: options are not used by applyFloodText — only startFloodText reads them.

import { applyFloodText, startFloodText, removeFloodText, getCleanHTML } from '@liiift-studio/floodtext'

const el = document.querySelector('p')
const original = getCleanHTML(el)
const opts = { effect: 'wght', amplitude: 200, period: 4, density: 2 }

let chars = applyFloodText(el, original)
let stop = startFloodText(chars, opts)

// On resize — re-wrap characters and restart (diagonal directions read BCR positions once):
const ro = new ResizeObserver(() => {
  stop()
  chars = applyFloodText(el, original)
  stop = startFloodText(chars, opts)
})
ro.observe(el)

// Later — stop the animation loop and restore the DOM:
stop()
ro.disconnect()
removeFloodText(el, original)

TypeScript

import type { FloodTextOptions, FloodEffect } from '@liiift-studio/floodtext'

const effects: FloodEffect[] = ['wght', 'oblique']
const opts: FloodTextOptions = { effect: effects, period: 4 }

Options

| Option | Default | Description | |--------|---------|-------------| | effect | 'wght' | 'wght' | 'wdth' | 'oblique' | 'opacity' | 'rotation' | 'blur' | 'size'. Pass an array to layer multiple effects simultaneously. Note: oblique requires Chrome 87+, Firefox 88+, Safari 14.1+. size causes layout recalculation per frame — use low amplitude | | amplitude | auto | Peak deviation from neutral. Used in single-effect mode. Defaults: wght 200, wdth 20, oblique 15°, opacity 0.3, rotation 15°, blur 2px, size 0.15em | | amplitudes | — | Per-effect overrides when layering multiple effects, e.g. { wght: 300, blur: 3 } | | properties | — | Custom CSS properties or variables to animate per character. Each entry: { property, base, amplitude, unit?, clamp? } where clamp is an optional [min, max] pair to cap the result (e.g. [0, 1] for opacity). E.g. [{ property: 'letter-spacing', base: 0, amplitude: 0.05, unit: 'em' }] or [{ property: '--my-axis', base: 100, amplitude: 20, clamp: [50, 150] }] | | period | 4 | Seconds per full wave cycle | | density | 2 | Wave cycles visible across the paragraph at once. Higher = more bands | | direction | 'diagonal-down' | 'diagonal-down' ↘ | 'diagonal-up' ↗ | 'right' → | 'left' ←. Diagonal directions use 2D screen coordinates; right/left use sequential character index | | waveShape | 'sine' | 'sine' | 'sawtooth' | 'triangle' | | as | 'p' | HTML element to render, e.g. 'h1', 'span'. (React component only) |


How it works

Every visible character is wrapped in an inline <span>. Whitespace is left as bare text nodes — no layout impact, no reflow. Each frame, the wave function is evaluated at that character's normalised position in the paragraph. The density option controls how many wave cycles are visible at once.

For direction: 'right' or 'left', position is the sequential character index — no DOM reads in the animation loop. For diagonal directions, each character's 2D screen coordinates are read via getBoundingClientRect once before the loop starts and projected onto the diagonal axis. Speed is framerate-independent; the loop excludes time the tab was hidden to prevent phase jumps when re-entering focus. In React, the animation loop is tied to the component lifecycle and stops automatically on unmount. The React hook skips the animation entirely if prefers-reduced-motion: reduce is set.

Line break safety: Character spans are inline (not inline-block), so applying styles per character does not change line widths or word breaks. The browser's natural line-breaking is fully preserved.


Dev notes

next in root devDependencies

package.json at the repo root lists next as a devDependency. This is a Vercel detection workaround — not a real dependency of the npm package. Vercel's build system inspects the root package.json to detect the framework; without next present it falls back to a static build and skips the Next.js pipeline, breaking the /site subdirectory deploy.

The package itself has zero runtime dependencies. Do not remove this entry.


Future improvements

  • Pause/resume API — explicit pause() and resume() methods rather than stop-and-restart, so animation phase is preserved across pauses
  • Intersection Observer — automatic pause when the element scrolls out of view; resume on re-entry
  • Per-character easing — apply a custom easing curve to individual character offsets, not just the raw wave value
  • More built-in effectshue (color hue rotation), shadow (text-shadow offset), skew (CSS skewX)
  • SSR-compatible static snapshot — render a stable mid-wave frame on the server so there is no FOUC before hydration

Current version: v1.0.3