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/ragtooth

v1.2.24

Published

Deliberate sawtooth rag for the web — works with React, vanilla JS, or any framework

Downloads

75

Readme

Ragtooth

npm License: MIT part of liiift type-tools

A sawtooth rag, on the web. Shapes text into alternating long/short lines — the kind of typographic rhythm that reads as design, not accident.

ragtooth.com · npm · GitHub

TypeScript · Zero dependencies · React + Vanilla JS


Install

npm install ragtooth

React

Component

import { RagText } from 'ragtooth'

<RagText sawDepth={120} sawPeriod={2}>
  Your paragraph text here...
</RagText>

Hook

import { useRag } from 'ragtooth'

const ref = useRag({ sawDepth: 120, sawPeriod: 2 })

<p ref={ref}>Your paragraph text here...</p>

useRag returns a ref to attach to any block element. Re-runs on resize automatically.

Vanilla JS

import { applyRag, removeRag } from 'ragtooth'

const el = document.querySelector('p')
const originalHTML = el.innerHTML

// Apply the rag
applyRag(el, originalHTML, { sawDepth: 120, sawPeriod: 2 })

// Remove it (restores original HTML)
removeRag(el, originalHTML)

Wait for fonts before measuring:

await document.fonts.ready
applyRag(el, el.innerHTML, { sawDepth: 120 })

How it works

Ragtooth measures each line's natural width by wrapping every word in a span, reading their offsetWidth, and grouping them into lines. It then applies max-width and letter-spacing to each line element to produce the sawtooth rhythm:

  • Long lines — stay at full container width
  • Short lines — constrained to containerWidth − sawDepth, with letter-spacing added to fill that reduced width

The algorithm never changes how text flows. It reads the browser's natural line breaks, then constrains them. ResizeObserver re-runs on any container width change.


Options

| Option | Type | Default | Description | |---|---|---|---| | sawDepth | RagValue | 80 | How far short lines are pulled in from full width. Higher = more pronounced sawtooth. | | sawPeriod | number | 2 | Lines per saw cycle. 2 = alternating long/short. 3 = two long, one short. 4 = three long, one short. | | sawPhase | number | sawPeriod | Which line in each cycle is shortened (1-indexed). Default = last line. | | sawAlign | 'top' \| 'bottom' | 'top' | Whether the cycle is anchored from the top or bottom of the block. 'bottom' guarantees the last lines are full-width. | | maxTracking | RagValue | 0.7 | Maximum letter-spacing any line can receive. Prevents grotesque stretching on very short lines. | | resize | boolean | true | Whether to re-run when the container resizes. Set to false for static contexts. |

RagValue

All size options (sawDepth, maxTracking) accept a RagValue — a number or a CSS-like string:

| Input | Resolves to | |---|---| | 80 | 80px | | "80px" | 80px | | "20%" | 20% of container width | | "2em" | 2× the element's computed font-size | | "1rem" | 1× the root font-size | | "5ch" | 5× the width of the "0" glyph |

sawAlign examples

// Guarantee the paragraph ends with two full-width lines
applyRag(el, el.innerHTML, {
  sawDepth: 100,
  sawPeriod: 3,
  sawAlign: 'bottom',
})
// Period of 3 from the bottom: lines count as [short, full, full] per group
// → the penultimate line is always full

sawPhase examples

// sawPeriod: 3, sawPhase: 2
// → pattern per 3-line group: [full, SHORT, full]
applyRag(el, el.innerHTML, { sawPeriod: 3, sawPhase: 2 })

TypeScript

import { applyRag, removeRag, getCleanHTML } from 'ragtooth'
import type { RagOptions, RagValue } from 'ragtooth'

const options: RagOptions = {
  sawDepth: '15%',
  sawPeriod: 3,
  sawAlign: 'bottom',
  maxTracking: '0.05em',
}

API reference

| Export | Description | |---|---| | applyRag(el, originalHTML, options?) | Applies the sawtooth rag to el. | | removeRag(el, originalHTML) | Restores el to its original HTML. | | getCleanHTML(el) | Returns the element's current HTML with all injected spans removed. | | useRag(options?) | React hook — returns a ref. Attaches, measures, and re-runs on resize. | | RagText | React component wrapper around useRag. | | RagOptions | TypeScript interface for all options. | | RagValue | Type for size options (number \| string). | | RAG_CLASSES | Object of CSS class names injected by the algorithm (rag-word, rag-line, etc.). |


Next.js

RagText and useRag require a browser environment. Add "use client" to any component that uses them:

"use client"
import { RagText } from 'ragtooth'

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.