optical margin alignment
Hang it right.
Every font.
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
"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
TypeScript + React · Vanilla JS
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
| 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 computed hang value in px. Characters whose hang falls below this are left flush. |
| maxHangRatio | 0.9 | Max proportion of advance width to hang (0–1). |
| hangFractions | see desc. | Per-character hang fraction overrides (0–1). Built-in defaults: hyphens & dashes 1.0; quotes, periods, !, ?, …, ), ] 0.8; (, [, commas, semicolons, colons 0.6. Pass your own map to override any character. |
no-code
Use it in Webflow, Framer & Figma
The same effect, no build step — drop it straight into your design tool.
Webflow
One script tag, then mark any element with data-opticalmargin. Configure it with data-* attributes.
<!-- Site Settings → Custom Code → Footer, or an Embed element -->
<script src="https://cdn.jsdelivr.net/npm/@liiift-studio/opticalmargin/dist/opticalmargin.webflow.min.js"></script>
<!-- Then add data-opticalmargin to any text element -->
<h1 data-opticalmargin>Your headline</h1>Framer
Insert → Code → New Component, then paste OpticalMargin.tsx ↗. It imports the core from esm.sh and exposes every option in the property panel — no build step.
import { /* core */ } from "https://esm.sh/@liiift-studio/opticalmargin"Figma · beta
Part of the Type Tools Figma plugin ↗ — Plugins → Development → Import plugin from manifest, run Type Tools, and pick this tool. Here it works with compromises — tracking or named-instance swaps (Figma can't set variable axes).