opticalmargin

Hang it right.
Every font.

npm ↗
GitHub
TypeScript·Canvas measurement·Cross-browser

CSS hanging-punctuationis Safari-only and has no weight control. Optical Margin measures each punctuation character's actual hang amount from Canvas font metrics and applies it as a margin. Works in every browser, with every font.

Live demo — toggle the hangs

Hang
Threshold (px)0.5
Max Hang Ratio0.90

"The best typography," wrote Jan Tschichold, "is invisible — it disappears into the reading." That is the paradox of the craft: the more perfectly it is executed, the less it is noticed. Every margin matters. Every spacing decision carries weight. "A quotation mark at the start of a line should hang," Bringhurst insists, "so that the letter, not the punctuation, holds the optical edge." The same applies to commas, dashes, periods — any mark smaller than a full letter. Hung correctly, the margin reads as a clean vertical. Left flush, it creates a slight indent that the eye registers as misalignment, even when the reader cannot name what bothers them. "It is a small thing," one might say — but in typography, every small thing is the thing.

Punctuation hangs at both margins.

Why not CSS?

hanging-punctuation is incomplete

The CSS property is Safari-only. It doesn't let you control hang amount, threshold, or which characters hang. And it uses hard-coded character tables, not the actual font metrics — so a T in one font hangs the same amount as a T in another.

Font-metric measurement

Canvas measureText returns both advance width and visual bounds. The difference is the optical hang amount — how far the character could move into the margin before it would look misaligned. This gives accurate results for every font without a lookup table.

Usage

Drop-in component

import { OpticalMarginText } from '@liiift-studio/opticalmargin'

<OpticalMarginText hangStart={true} hangEnd={true}>
  "Your paragraph text here..."
</OpticalMarginText>

Hook

import { useOpticalMargin } from '@liiift-studio/opticalmargin'

const ref = useOpticalMargin({ hangStart: true, hangEnd: true })
<p ref={ref}>{children}</p>

Vanilla JS

import { applyOpticalMargin, removeOpticalMargin, getCleanHTML } from '@liiift-studio/opticalmargin'

const el = document.querySelector('p')
const original = getCleanHTML(el)
applyOpticalMargin(el, original, { hangStart: true, hangEnd: true })

// Later — restore original:
removeOpticalMargin(el, original)

Options

OptionDefaultDescription
hangStarttrueHang opening punctuation at line starts.
hangEndtrueHang closing punctuation and sentence-end marks at line ends.
threshold0.5Minimum hang amount in px before applying.
maxHangRatio0.9Max proportion of advance width to hang (0–1).