optical margin alignment
Native hanging-punctuation has uneven browser support and specific first- and last-line behavior. Optical Margin offers adjustable punctuation hanging on eligible internal lines too. It measures character advances and applies a fraction; you decide whether the result suits the typeface. Tested cases and limits are documented below.
Live demo — toggle the hangs, change the alignment, resize the column
“Start with the text box,” the editor suggests, “then look at what the letters actually draw.” The guide marks the measured edge. A small opening quote can sit inside that edge and leave the following letter farther in. “Try a narrower column.” The browser wraps the words again, and a different quotation may now start an internal line. “Should this mark hang farther?” That is a design choice: the same fraction can look different in another typeface. Compare the opening marks with commas and full stops at the other edge. “Now switch the alignment.” A ragged edge does not supply the same reference as an aligned one. These sentences are a specimen for experimenting with punctuation; they do not establish a preferred amount or a reading benefit.
Justified text aligns both edges, so punctuation hangs at both margins.
Research
Which Margin Are We Aligning?
Native CSS and manual indents first; then a real internal-line quote in Chromium. A bounded case study of measured advances and author judgment.
Why not CSS?
hanging-punctuation is incomplete
The CSS property describes eligible edges and punctuation with keywords. It does not expose a numeric hang fraction or a threshold, and its first/last rules do not request general Latin internal-line alignment. Prefer native CSS when its semantics and browser support meet the design.
Font-metric measurement
A mark that starts or ends a line is measured where it sits on the page, in the rendered font — size, variation settings and letter-spacing included. A set fraction of that width hangs into the margin: dashes fully, quotes and periods at 80%, commas at 60%. The fractions are adjustable per character.
The text stays one flow
Lines are never locked. Words that begin or end with a hangable mark are wrapped in place in a plain span, and the browser keeps breaking lines as usual — so hyphenation and ­ keep working, a link that wraps stays one link, and copy-paste gives the original text. Resize the column and the hangs move to the marks that now start or end a line.
Aligned edges only
Opening marks hang at the start edge of left-aligned and justified text; closing marks hang at the end edge of right-aligned and justified text. A ragged edge has nothing to align, so it is left alone. A mark whose hang would pull its word up to the line above is left flush.
Usage
TypeScript + React · Vanilla JS
Drop-in component
import { OpticalMarginText } from '@overpunch/opticalmargin'
<OpticalMarginText hangStart={true} hangEnd={true}>
"Your paragraph text here..."
</OpticalMarginText>Hook
import { useOpticalMargin } from '@overpunch/opticalmargin'
const ref = useOpticalMargin({ hangStart: true, hangEnd: true })
<p ref={ref}>{children}</p>Vanilla JS
import { applyOpticalMargin, removeOpticalMargin, getCleanHTML } from '@overpunch/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. Shows on start-aligned and justified text. |
| hangEnd | true | Hang closing punctuation and sentence-end marks at line ends. Shows on justified and end-aligned text. |
| 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/@overpunch/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/@overpunch/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).