@liiift-studio/opticalmargin
v1.0.10
Published
Font-metric hanging punctuation — cross-browser optical margin alignment using actual glyph bounds, not a lookup table
Readme
Optical Margin
CSS hanging-punctuation is Safari-only, uses hard-coded character tables, and gives no control over hang amount, threshold, or which characters hang. Optical Margin measures each punctuation character's actual hang amount from Canvas font metrics — not a lookup table — and applies it as a negative margin. Works in every browser, with every font.
opticalmargin.com · npm · GitHub
TypeScript · Canvas measurement · Cross-browser · React + Vanilla JS
Install
npm install @liiift-studio/opticalmarginUsage
Next.js App Router: this library uses browser APIs. Add
"use client"to any component file that imports from it.
React component
import { OpticalMarginText } from '@liiift-studio/opticalmargin'
<OpticalMarginText>
"Typography is the craft of endowing human language with a durable visual form."
</OpticalMarginText>Both hangStart and hangEnd default to true, so no props are required for standard use.
React hook
import { useOpticalMargin } from '@liiift-studio/opticalmargin'
// Inside a React component:
const ref = useOpticalMargin()
return <blockquote ref={ref}>{children}</blockquote>The hook re-runs automatically on resize via ResizeObserver and after fonts load via document.fonts.ready.
Vanilla JS
import { applyOpticalMargin, removeOpticalMargin, getCleanHTML } from '@liiift-studio/opticalmargin'
const el = document.querySelector('blockquote')
const original = getCleanHTML(el)
const opts = { hangStart: true, hangEnd: true }
function run() {
applyOpticalMargin(el, original, opts)
}
run()
document.fonts.ready.then(run)
const ro = new ResizeObserver(() => run())
ro.observe(el)
// Later — disconnect and restore original markup:
// ro.disconnect()
// removeOpticalMargin(el, original)TypeScript
import type { OpticalMarginOptions } from '@liiift-studio/opticalmargin'
const opts: OpticalMarginOptions = { threshold: 1, maxHangRatio: 0.8 }Options
| Option | Default | Description |
|--------|---------|-------------|
| hangStart | true | Hang opening punctuation at line starts |
| hangEnd | true | Hang closing punctuation and sentence-end marks at line ends |
| threshold | 0.5 | Minimum hang amount in px before applying. Prevents near-zero corrections on characters that barely protrude |
| maxHangRatio | 0.9 | Max proportion of the character's advance width to hang (0–1). Caps extreme hangs on very wide punctuation |
| as | 'p' | HTML element to render, e.g. 'blockquote', 'h1'. (React component only) |
How it works
Canvas measureText returns both width (advance width) and actualBoundingBoxLeft / actualBoundingBoxRight (visual bounds). The difference between advance width and visual bounds is the optical overhang — how far a character's ink sits inside its typographic cell. That value, clamped by maxHangRatio and threshold, is applied as margin-inline-start (start hang) or margin-inline-end (end hang) on each line span. Using logical properties means the direction is correct in both LTR and RTL contexts. The algorithm re-runs on resize and after fonts finish loading (document.fonts.ready).
Start character set: " ' " ' « ( [
End character set: . , ; : ! ? " ' " ' » - – — … ) ]
Falls back to zero hang (no margin applied) in environments without Canvas support (e.g. SSR).
Line break safety: Line breaks are locked to the browser's natural layout. Word breaks never change — the negative margins only affect the optical edge position, not line content or width.
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
- Hanging numerals — detect and hang numerals (
1,7) that protrude into the margin at display sizes - Configurable character set — expose a
hangCharsoption to override which characters are considered candidates, beyond the built-in punctuation list - Per-side max hang — separate
maxHangStart/maxHangEndratios for asymmetric control - RTL auto-detection — automatically swap start/end hang sides based on the element's computed
directionstyle - Intersection Observer — skip measurement for off-screen elements and re-run when they enter the viewport
Current version: v1.0.9
