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

torph

v0.1.3

Published

Dependency-free animated text component.

Downloads

252,168

Readme

Torph

Dependency-free animated text morphing component for React, Vue, Svelte, and vanilla JavaScript.

Installation

npm install torph
# or
pnpm add torph
# or
yarn add torph

Framework Usage

React

import { TextMorph } from "torph/react";

function App() {
  const [text, setText] = useState("Hello World");

  return (
    <TextMorph
      duration={400}
      ease="cubic-bezier(0.19, 1, 0.22, 1)"
      locale="en"
      onAnimationComplete={() => console.log("Animation done!")}
      className="my-text"
      as="h1"
    >
      {text}
    </TextMorph>
  );
}

React Hook

import { useTextMorph } from "torph/react";

function CustomComponent() {
  const { ref, update } = useTextMorph({
    duration: 400,
    ease: "cubic-bezier(0.19, 1, 0.22, 1)",
  });

  useEffect(() => {
    update("Hello World");
  }, []);

  return <div ref={ref} />;
}

Vue

<script setup>
import { ref } from "vue";
import { TextMorph } from "torph/vue";

const text = ref("Hello World");

const handleComplete = () => {
  console.log("Animation done!");
};
</script>

<template>
  <TextMorph
    :text="text"
    :duration="400"
    ease="cubic-bezier(0.19, 1, 0.22, 1)"
    locale="en"
    :onAnimationComplete="handleComplete"
    class="my-text"
    as="h1"
  />
</template>

Svelte

<script>
  import { TextMorph } from 'torph/svelte';

  let text = $state('Hello World');

  const handleComplete = () => {
    console.log('Animation done!');
  };
</script>

<TextMorph
  {text}
  duration={400}
  ease="cubic-bezier(0.19, 1, 0.22, 1)"
  locale="en"
  onAnimationComplete={handleComplete}
  class="my-text"
  as="h1"
/>

Vanilla JS

import { TextMorph } from "torph";

const morph = new TextMorph({
  element: document.getElementById("morph"),
  duration: 400,
  ease: "cubic-bezier(0.19, 1, 0.22, 1)",
  locale: "en",
  onAnimationStart: () => console.log("Starting..."),
  onAnimationComplete: () => console.log("Done!"),
});

morph.update("Hello World");

Spring Animations

Pass spring parameters to ease for physics-based easing. The duration is computed automatically from the spring physics.

import { TextMorph } from "torph/react";

function App() {
  const [text, setText] = useState("Hello World");

  return (
    <TextMorph ease={{ stiffness: 200, damping: 20 }}>{text}</TextMorph>
  );
}

Spring Parameters

| Parameter | Type | Default | Description | | ----------- | -------- | ------- | ----------------------------------------- | | stiffness | number | 100 | Spring stiffness coefficient | | damping | number | 10 | Damping coefficient | | mass | number | 1 | Mass of the spring | | precision | number | 0.001 | Threshold for determining settled position |

Numbers

Numeric words morph by place value: digits slide along the block axis, and the symbols around them — currency, separators, signs, suffixes — travel with the places they belong to. It is on by default, so any value that contains a number already animates this way. Pass numbers={false} to fall back to the character-level text morph.

import { TextMorph } from "torph/react";

// 1,204 → 1,318 rolls the hundreds and tens, leaves the thousands alone
<TextMorph>{`$${total.toLocaleString("en")}`}</TextMorph>;

A value passed as a number rather than a string is formatted for you, so locale and decimals apply:

<TextMorph decimals={2} locale="de-DE">
  {1234.5}
</TextMorph>

Editable fields

Place matching is the right default for a value that changes on its own — a counter, a total, a chart readout. It is the wrong one for a field somebody is typing in, where the character that just changed is known and place value is not the point: typing 1 in front of 20 should insert a digit, not renumber the column.

Pass cursorIndex to switch that update from place matching to caret matching. It is available on the React component and as the second argument to update(); a caret position is a DOM concern, so there is no Vue or Svelte prop for it.

const [value, setValue] = useState("");
const [caret, setCaret] = useState<number>();

<input
  value={value}
  onChange={(e) => {
    setCaret(e.target.selectionStart ?? undefined);
    setValue(e.target.value);
  }}
/>
<TextMorph cursorIndex={caret}>{value}</TextMorph>
morph.update("$120", 2);

API

Options

All components accept the following props/options:

  • text / children: string - The text to display (required)
  • duration?: number - Animation duration in milliseconds (default: 400). Ignored when ease is a spring, which settles on its own physics
  • ease?: string | SpringParams - CSS easing function or spring parameters (default: "cubic-bezier(0.19, 1, 0.22, 1)")
  • scale?: boolean - Enable scale animation on exiting segments (default: true)
  • numbers?: boolean - Morph numeric words by place value, sliding digits along the block axis. Off falls back to the character-level text morph (default: true)
  • decimals?: number - Fraction digits to format a numeric value to. Applies when the value is a number (React children, or update(number)); ignored for strings
  • locale?: Intl.LocalesArgument - Locale for text segmentation, and for formatting a numeric value (default: "en")
  • debug?: boolean - Enable debug mode with visual indicators
  • disabled?: boolean - Disable all morphing animations (default: false)
  • respectReducedMotion?: boolean - Respect user's prefers-reduced-motion setting (default: true)
  • onAnimationStart?: () => void - Callback fired when animation begins
  • onAnimationComplete?: () => void - Callback fired when animation completes
  • onAnimationCancel?: () => void - Callback fired when a morph is interrupted by the next one. Exactly one of onAnimationComplete and onAnimationCancel runs per morph
  • className?: string - CSS class name (React/Vue: class)
  • style?: object | string - Inline styles
  • as?: string - HTML element type (default: "span")

Multi-line text

Newlines in a value are rendered as line breaks, and text morphs normally across them. The React component includes them in its server-rendered markup; Vue and Svelte render them once the component mounts.

Utilities

The text matching torph runs on is exported for building your own behaviour on top of it:

  • segmentText(value, locale): Segment[] - Split a value into segments
  • diffSegments(oldSegments, newText, locale): DiffResult - Match a new value against existing segments, returning the segments that persist, enter and exit

Found this useful?

Follow me on Twitter.

Other projects

You might also like:

Acknowledgements

  • Thanks to Alex for assistance with the site design.
  • Thanks to Pugson for putting up with my bullshit.
  • Thanks to Benji for coining the Torph name and outlining the method in Family Values.

License

MIT