ragtooth
A Sawtooth Rag,
on the web.
Most tools fight your rag. Ragtooth works with it — shaping text into a sawtooth pattern of alternating long and short lines. The kind of rhythm that reads as design, not accident. A technique from editorial typesetting, now in one fully-typed npm package.
Live demo — drag the sliders
What is sawtooth rag?
The problem with smooth rag
When text is set ragged-right, natural line endings create an unpredictable right edge — notches, peninsulas, near-rivers. It can look accidental. Most tools patch this with soft hyphens or non-breaking spaces: a lot of effort for a result that’s still a mess.
The case for sawtooth rag
A sawtooth pattern — long line, short line, long line — gives the rag a rhythm. Structured, not random. The eye reads it as a choice. Book typographers and editorial designers have used it for decades. Now it takes thirty seconds.
Usage
TypeScript + React · Vanilla JS
Drop-in component
import { RagText } from '@liiift-studio/ragtooth'
<RagText sawDepth={120} sawPeriod={2}>
Your paragraph text here...
</RagText>Hook — attach to any element
import { useRag } from '@liiift-studio/ragtooth'
const { ref } = useRag({ sawDepth: 120, sawPeriod: 2 })
<p ref={ref}>{children}</p>Vanilla JS
import { applyRag, removeRag, getCleanHTML } from '@liiift-studio/ragtooth'
const el = document.querySelector('p')
// getCleanHTML strips any previously-injected spans before storing original HTML
const original = getCleanHTML(el)
applyRag(el, original, { sawDepth: 120, sawPeriod: 2 })
// To remove the effect and restore original markup:
removeRag(el, original)Options
| Option | Default | Description |
|---|---|---|
| sawDepth | 80 | How much shorter the short lines are, in px. Higher = more pronounced sawtooth. Also accepts %, em, rem, ch as a string. |
| sawPeriod | 2 | Number of lines per cycle. With period 2 the pattern is long–short–long–short. With period 3 it is long–long–short. The short line's position within the cycle is controlled by sawPhase. |
| sawPhase | sawPeriod | Which line within each cycle is shortened (1-indexed). Defaults to the last line of each cycle. Use with sawPeriod to place the short line exactly where you want it. |
| sawAlign | 'top' | Anchor the cycle to the top or bottom of the block. 'bottom' with sawPeriod 3 keeps the last two lines full — no awkward short penultimate line. |
| maxTracking | 0.7 | Max letter-spacing in px (also accepts em, rem). Keeps lines from being stretched into oblivion. |
| resize | true | Re-runs the algorithm on container resize via ResizeObserver. Set false for static layouts. |
| ragDifference | — | Deprecated alias for sawDepth. Will be removed in a future major version. |
Word boundaries are detected using Intl.Segmenterwhen available — correctly splitting CJK (Chinese, Japanese, Korean), Arabic, Thai, and other scripts that don’t use spaces as word delimiters. Falls back to a whitespace regex in environments without Segmenter support.