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

@tonybonet/chop

v2.0.0

Published

Pretext-powered text splitting for words, lines, characters, and paragraphs

Downloads

104

Readme

@tonybonet/chop

Pretext-powered text splitting for animation systems. Chop gives you words, lines, characters, and paragraphs that match canvas-measured layout while the original DOM text stays accessible.

Install

pnpm add @tonybonet/chop
npm install @tonybonet/chop

Pick The API

| Need | Use | You get | | --- | --- | --- | | Animate existing browser text | chop(source) | DOM elements | | Measure text and render your own spans | layoutText(text, font, options) | layout handles | | Reuse a font string safely | defineFont(cssFont) | typed font descriptor | | Select subsets for stagger logic | select(handles, query) | filtered handles | | Clear long-lived measurement caches | clearPretextCache() | cache cleanup |

DOM API

import { chop } from "@tonybonet/chop";

const title = chop(".hero");

title is an array of word elements, so it works directly with GSAP, WAAPI, Motion, or plain DOM code.

gsap.from(title, {
  y: 40,
  opacity: 0,
  stagger: 0.05,
});

Extra units and metadata live on the same array:

title.lines; // readonly HTMLElement[]
title.chars; // readonly HTMLElement[]
title.paragraphs; // readonly HTMLElement[]
title.meta; // ChopMeta
title.metrics; // ChopFontMetrics | null
title.relayout(); // recompute and reproject
title.destroy(); // remove overlay and restore the source element

Words are projected immediately. Lines, characters, and paragraphs are lazy, so this:

const title = chop(".hero");

does not create character spans until you ask for them:

title.chars;

Pure Layout API

Use pure mode when a framework should own the markup.

import { defineFont, layoutText } from "@tonybonet/chop";

const font = defineFont("700 48px Inter");
const result = layoutText("Hello world", font, {
  width: 360,
  whiteSpace: "normal",
});

result.words.map((word) => word.text);
result.meta.lineCount;

Pure mode does not touch the DOM, but it still needs a browser-like canvas environment because Pretext measures text. In SSR, run it on the client or provide a canvas implementation.

ChopMeta

ChopMeta is the summary of the measured layout. It is not animation state. It answers questions like "how many words do I have?" and "did this wrap?".

interface ChopMeta {
  readonly lineCount: number;
  readonly wordCount: number;
  readonly charCount: number;
  readonly paragraphCount: number;
  readonly height: number;
  readonly naturalWidth: number;
  readonly preserveLineBreaks: boolean;
  readonly preserveSpaces: boolean;
  readonly keepWordsTogether: boolean;
  readonly fontMetrics: ChopFontMetrics | null;
}

Example:

const headline = chop(".headline");

const target = headline.meta.lineCount > 1 ? headline.lines : headline;

gsap.from(target, {
  yPercent: 100,
  stagger: 0.08,
});

Handles

layoutText() returns handles. A handle is a layout record for one unit of text.

interface ChopHandle {
  id: string;
  unit: "line" | "word" | "char" | "paragraph";
  text: string;
  index: number;
  lineIndex?: number;
  wordIndex?: number;
  paragraphIndex?: number;
  isWhitespace: boolean;
  start: { segmentIndex: number; graphemeIndex: number };
  end: { segmentIndex: number; graphemeIndex: number };
  rect?: ChopRect;
}

Handles preserve relationships. A character can know which word and line it belongs to. A word can know which paragraph it belongs to. That keeps custom rendering and stagger logic straightforward.

const result = layoutText("Make text move", "500 32px Inter");

const firstLineChars = result.chars.filter((char) => char.lineIndex === 0);

Options

interface ChopTextOptions {
  width?: number;
  whiteSpace?: "normal" | "pre-wrap";
  wordBreak?: "normal" | "keep-all";
  locale?: string;
  // Absolute pixels. CSS multipliers like 1.5 are not accepted by Pretext.
  lineHeight?: number;
  // Absolute pixels. Negative values follow CSS behavior.
  letterSpacing?: number;
}

DOM mode infers typography from the element. Pure mode needs the font passed in.

Selection

import { layoutText, select } from "@tonybonet/chop";

const result = layoutText("Chop splits real text", "700 40px Inter");

select(result.chars, { mode: "even" });
select(result.words, { mode: "first", count: 2 });
select(result.chars, {
  mode: "grouped",
  by: "word",
  query: { mode: "last", count: 1 },
});

API Litmus Test

Chop keeps one primary shape:

| Question | Answer | | --- | --- | | Animate words | gsap.from(chop(".hero"), vars) | | Need another DOM unit | title.lines, title.chars, title.paragraphs | | Need measurement only | layoutText(text, font, options) | | Need a subset | select(result.chars, query) or native array methods |

There is no duplicate .words, no .elements(unit), and no wrapper object. The array itself is the word list.

CSS Hooks

Projected elements receive stable data attributes and CSS custom properties.

| Unit | Attribute | Main index variable | | --- | --- | --- | | line | [data-chop-line] | --chop-line-index | | word | [data-chop-word] | --chop-word-index | | char | [data-chop-char] | --chop-char-index | | paragraph | [data-chop-paragraph] | --chop-paragraph-index |

[data-chop-char] {
  opacity: 0;
  animation: fade-in 0.3s calc(var(--chop-char-index) * 40ms) both;
}

React Pattern

For DOM projection, keep the Chop instance in a ref and clean it up from the ref callback.

import { chop, type ChopElements } from "@tonybonet/chop";
import { useCallback, useRef } from "react";

function Heading() {
  const instance = useRef<ChopElements | null>(null);

  const setNode = useCallback((node: HTMLHeadingElement | null) => {
    instance.current?.destroy();
    instance.current = node ? chop(node) : null;
  }, []);

  return <h1 ref={setNode}>Text with real layout</h1>;
}

Do not put ChopElements in React state. It is an imperative DOM projection.

Migration From The Old API

| Old | New | | --- | --- | | chop(node, { by: ["word"] }).elements("word") | chop(".headline") | | instance.elements("line") | title.lines | | instance.elements("char") | title.chars | | instance.elements("paragraph") | title.paragraphs | | instance.meta() | title.meta | | instance.refresh() | title.relayout() | | chop(text, font, options) | layoutText(text, font, options) |

Accessibility

Chop keeps the source text in the DOM, makes it visually transparent, and mounts an aria-hidden overlay for animation. Screen readers keep reading the original semantic text.

License

MIT.