
Justif is a JavaScript library that applies TeX‑style text justification to existing HTML paragraphs.
It uses the Knuth–Plass line‑breaking algorithm to evaluate a paragraph as a whole, choosing break points that produce more even word spacing than the browser’s default line‑by‑line justification.
The result is publication‑grade typesetting that reduces rivers of whitespace and awkward gaps, with automatic hyphenation, hanging punctuation, and subtle letter‑spacing or variable‑font width adjustments when needed.
The library works as a progressive enhancement. You keep text‑align: justify in your CSS for native rendering, then add a single module script to upgrade supported paragraphs.
Justif leaves unsupported elements (such as mixed‑direction text or contenteditable) untouched. Your page always has a usable fallback.
Features
- Paragraph-wide Knuth-Plass line breaking.
- Language-aware hyphenation with bundled TeX patterns.
- Optical margin alignment and hanging punctuation.
- Adjustable word spacing and letter tracking.
- Variable font expansion through the
wdthaxis. - CJK justification with Japanese kinsoku line-breaking rules.
- Automatic reflow after container and web font changes.
- Native CSS fallback for unsupported paragraphs.
- Preserved inline links, emphasis, code, and text selection.
- Lifecycle controls for refreshing or restoring the original DOM.
How To Use It
Basic Usage
Keep native text justification in your stylesheet. Justif only enhances elements whose computed text-align value is justify or justify-all.
Set the document language as well. The automatic build uses the closest lang attribute to select an available hyphenation pattern.
The automatic build scans p, li, dd, blockquote, and figcaption elements by default. The data-justif-selector attribute narrows that scan to a specific content area.
<html lang="en-US">
<head>
<style>
.article-copy p {
max-width: 42rem;
text-align: justify;
}
</style>
<script
type="module"
blocking="render"
data-justif-selector=".article-copy p"
src="https://cdn.jsdelivr.net/npm/justif/dist/auto.js"
crossorigin="anonymous"
></script>
</head>
<body>
<article class="article-copy">
<p>
Long-form editorial text often exposes weak line breaks and inconsistent
spacing when the browser justifies each line independently.
</p>
</article>
</body>
</html>
Automatic Script Controls
Add data-justif-selector to choose which elements enter the automatic scan.
<script type="module" data-justif-selector="main article p, main article blockquote" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/auto.js" ></script>
Add data-justif-debug during development to log the reason behind every skipped paragraph.
<script type="module" data-justif-selector=".prose p" data-justif-debug src="https://cdn.jsdelivr.net/npm/[email protected]/dist/auto.js" ></script>
The automatic script also creates a window.justif object with the following members:
justifyunjustifycontrollersbooted
Await window.justif.booted before destroying every automatic controller. Language modules may finish loading after the first controller appears.
await window.justif.booted;
for (const controller of window.justif.controllers) {
controller.destroy();
}
NPM Installation
Install the package when the application already uses ES modules or a bundler.
npm install justif
Import the main function and a hyphenator for the target language.
import { justify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";
const paragraphs = document.querySelectorAll(".article-copy p");
const controller = justify(paragraphs, {
hyphenate: hyphenateEnUS,
});
justify() applies its first layout before returning the controller. The ready promise becomes useful when another operation must wait for web fonts and the resulting font-driven layout to settle.
await controller.ready; // Position annotations after the final font metrics are available. positionArticleAnnotations();
Apply Different Hyphenators by Language
The JavaScript API expects one hyphenator for each call. Group multilingual paragraphs by their declared language.
import { justify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";
import { hyphenateDe } from "justif/hyphenate/de";
const englishController = justify(
document.querySelectorAll('article p:lang(en)'),
{
hyphenate: hyphenateEnUS,
}
);
const germanController = justify(
document.querySelectorAll('article p:lang(de)'),
{
hyphenate: hyphenateDe,
}
);
A custom hyphenator receives a lowercase word and returns fragments that join back into the same word.
const exceptions = new Map([
["microtypography", ["mi", "cro", "ty", "pog", "ra", "phy"]],
]);
const customHyphenator = (word) => {
return exceptions.get(word) ?? [word];
};
justify(document.querySelectorAll(".special-copy p"), {
hyphenate: customHyphenator,
});
Author-written soft hyphens work even when no hyphenate callback is present. Apply hyphens: none to inline code, product names, or other content that must remain intact.
.article-copy code,
.article-copy .product-name {
hyphens: none;
}
Create Rectangular Paragraphs
The default lastLineMinWidth value targets an ending that reaches at least one third of the available measure.
Set the value to 1 when the layout should attempt a fully rectangular paragraph. The algorithm keeps natural spacing when a full final line would require an unacceptable result.
import { justify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";
justify(document.querySelectorAll(".rectangular-copy p"), {
hyphenate: hyphenateEnUS,
lastLineMinWidth: 1,
});
CSS text-align: justify-all also selects the rectangular mode in the automatic build.
.rectangular-copy p {
text-align: justify-all;
}
Handle Dynamic Paragraph Content
Call refresh() after a width change that Justif does not observe. Content changes and computed text-style changes require a fresh scan.
Destroy the old controller, update the content, and call justify() again.
import { justify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";
const article = document.querySelector(".live-article");
let controller = justify(article.querySelectorAll("p"), {
hyphenate: hyphenateEnUS,
});
function replaceArticleContent(html) {
controller.destroy();
article.innerHTML = html;
controller = justify(article.querySelectorAll("p"), {
hyphenate: hyphenateEnUS,
});
}
refresh() remeasures the existing scan. It does not rescan changed text, new inline elements, or new computed typography rules.
Track Paragraph Relayouts
The onRelayout callback runs after the initial enhancement, observed width changes, explicit refreshes, web font changes, and transitions between native one-line and enhanced multiline states.
const controller = justify(document.querySelectorAll(".annotated-copy p"), {
hyphenate: hyphenateEnUS,
onRelayout(paragraph) {
updateAnnotationsFor(paragraph);
},
});
Use onSkip to inspect paragraphs that stay under native browser layout.
justify(document.querySelectorAll(".article-copy p"), {
hyphenate: hyphenateEnUS,
onSkip(paragraph, reason) {
console.info("Justif skipped:", paragraph, reason);
},
});
Configuration Options
Core Layout Options
hyphenate(function, defaultundefined): Splits a lowercase word into hyphenatable fragments. The returned fragments must reproduce the original word when joined.protrusion(boolean or object, defaulttrue): Controls optical margin alignment. Passfalseto disable protrusion or supply a character table to override the built-in Latin values.hangingPunctuation(boolean or string, default"line-end-only"): Controls full punctuation hanging. Use"line-end-only","first-line-and-line-ends","all-line-edges", ornone.expansion(object orfalse, default{ max: 0.02, shrink: 0.02, step: 0.005 }): Adjusts thewdthaxis of a compatible variable font for each line.tracking(boolean or object, default{ max: 0.03, shrink: 0.03 }): Applies small per-line letter-spacing changes. Passfalseto disable tracking.spacing(object, default{ stretch: 0.5, shrink: 1/3, pull: 0.7, boundaryShrink: 0 }): Sets word-space expansion, contraction, secondary-font matching, and font-boundary behavior.lastLineMinWidth(number, default0.33): Sets the desired minimum width of a paragraph ending as a fraction of the full measure. Use0to disable it or1to request rectangular paragraphs.lastLineFit(number, default0): Applies a fraction of the paragraph’s average spacing adjustment to the final line.observeResize(boolean, defaulttrue): Reflows managed paragraphs when their content width changes.cleanClipboard(boolean, defaulttrue): Removes layout-only word joiners and generated nonbreaking spaces from copied content.
Line-Breaking Tuning
Most projects should keep these values at their defaults.
tolerance(number, default200): Sets the highest accepted line badness after hyphenation becomes available.pretolerance(number, default100): Sets the highest accepted badness before the algorithm tries automatic hyphenation. A negative value skips this pass.linePenalty(number, default10): Adds a base cost for each line. Higher values favor solutions with fewer lines.hyphenPenalty(number, default50): Sets the cost of an automatic hyphenation break.exHyphenPenalty(number, default50): Sets the cost of a break after a hyphen already present in the text.adjDemerits(number, default10000): Penalizes sharply different spacing on adjacent lines.doubleHyphenDemerits(number, default10000): Penalizes automatic hyphens on consecutive lines.finalHyphenDemerits(number, default5000): Penalizes a hyphen on the line immediately before the final line.emergencyStretch(number or"auto", default"auto"): Adds extra word-space flexibility after normal layout passes fail. The automatic value is approximately3em.
Callback Options
onRelayout(function, defaultundefined): Receives a paragraph after a meaningful rendered layout change.onSkip(function, defaultundefined): Receives a skipped paragraph and a human-readable reason.
Expansion, Tracking, and Spacing Values
Microtypography values use fractions. A value of 0.02 represents 2 percent.
expansion.max: Sets the maximum variable-font widening. The default0.02permits a width of up to 102 percent.expansion.shrink: Sets the maximum variable-font narrowing. The default0.02permits a width down to 98 percent.expansion.step: Sets the interval used when testing width-axis values. The default0.005uses half-percent steps.tracking.max: Sets the maximum line widening through added letter spacing. The default permits a 3 percent increase.tracking.shrink: Sets the normal limit for reduced letter spacing. The default permits a 3 percent decrease.spacing.stretch: Sets how far word spaces may expand. The default0.5permits spaces up to 150 percent of their natural width.spacing.shrink: Sets how far word spaces may contract. The default1/3permits spaces down to about 67 percent of their natural width.spacing.pull: Pulls wide spaces from secondary fonts toward the base font’s space width. The accepted range is0to1.spacing.boundaryShrink: Controls shrinking at font-family boundaries such as inline code, badges, and keyboard labels. The accepted range is0to1.
API Methods and Controller
import { justify, unjustify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";
const paragraphs = document.querySelectorAll(".article-copy p");
// Enhance one element or any iterable of elements.
const controller = justify(paragraphs, {
hyphenate: hyphenateEnUS,
});
// Wait for relevant web fonts and font-driven layouts to settle.
await controller.ready;
// Reuse the existing paragraph scan and remeasure the layout.
controller.refresh();
// Access the elements selected by this controller.
console.log(controller.paragraphs);
// Restore the original DOM and disconnect this controller's observers.
controller.destroy();
// Restore managed elements when the original controller is unavailable.
unjustify(document.querySelectorAll(".article-copy p"));
The controller exposes two methods:
refresh()remeasures the existing paragraph scan.destroy()restores the original DOM and disconnects observers.
It also exposes two read-only properties:
readyis a promise that settles after relevant font loading and layout work finishes.paragraphscontains the selected paragraph elements.
Loading and First Paint
The optional blocking="render" attribute asks supporting browsers to wait for the module before the first paint. This prevents native justification from appearing briefly before Justif applies its layout.
<script type="module" blocking="render" src="https://cdn.jsdelivr.net/npm/[email protected]/dist/auto.js" ></script>
This choice delays the first paint until the script downloads and executes. Remove the attribute when faster initial rendering matters more than avoiding a visible typography change.
Some browsers do not treat the module as render-blocking. Those browsers may paint the native justified paragraph first.
Hyphenation patterns for languages other than American English load as separate modules. The first rendered state may contain Justif spacing before the corresponding pattern adds hyphenated breaks.
Self-hosted installations should preserve the package’s complete dist/ directory structure. The automatic build loads language files through relative dynamic imports.
Web fonts also affect line measurement. Preload important fonts and choose fallback fonts with similar metrics to reduce visible reflow.
Optimize Long Pages
Browser layout work may become noticeable when an article contains many long paragraphs. CSS content-visibility keeps off-screen text out of immediate rendering work.
.long-article p {
content-visibility: auto;
contain-intrinsic-size: auto 8em;
}
Justif keeps placeholder heights aligned with the managed paragraph layout, which helps maintain stable scrollbars and anchor positions.
Supported Content
Justif supports horizontal left-to-right text, CJK text, and paragraphs written entirely in right-to-left Hebrew or Arabic. Automatic hyphenation does not run for right-to-left paragraphs.
The following inline content may wrap across generated lines:
- Links.
- Emphasized and strongly emphasized text.
- Inline code.
- Keyboard labels.
- Badges and highlighted spans.
- Inline elements with horizontal padding and borders.
- Elements with
white-space: nowrap.
Computed font variants and low-level font feature settings remain part of text measurement.
Hard <br> elements remain actual line breaks. A line ending at <br> stays ragged by default. Apply text-align-last: justify when those lines should receive full justification.
.poem-with-hard-breaks {
text-align: justify;
text-align-last: justify;
}
Native Fallback Conditions
A paragraph stays under native browser layout when Justif cannot reproduce its structure reliably. Common examples include:
- Mixed left-to-right and right-to-left text in one paragraph.
- Vertical writing modes.
- Thai and Lao text.
- Images, form controls, SVG, or MathML.
- Floats or block-level descendants.
- Inline descendants with horizontal margins.
box-decoration-break: clone.- Preserved-whitespace modes.
contenteditableparagraphs.
Keep text-align: justify in the stylesheet for these cases. One skipped paragraph does not prevent nearby supported paragraphs from receiving enhanced layout.
Inline badges and code tokens should use padding for their internal spacing. Horizontal margins may cause the paragraph to remain under native layout.
Interactive Inline Elements
Justif renders managed inline descendants as clones. Event listeners attached directly to the original child elements do not transfer to those clones.
Use event delegation from a stable ancestor.
const article = document.querySelector(".article-copy");
article.addEventListener("click", (event) => {
const trigger = event.target.closest("[data-footnote]");
if (!trigger) {
return;
}
openFootnote(trigger.dataset.footnote);
});
Existing JavaScript references continue to point to the original descendants. Direct listeners become active again after destroy() restores the original DOM.
Set options in CSS
:root {
--justif-tracking: none;
--justif-last-line-min-width: 50%;
}
blockquote {
--justif-hanging-punctuation: none;
}Alternatives
- Enhance Text Readability with Balance Text JavaScript Library
- CSS Hyphenation Polyfill: Hyphenopoly.js
FAQs
Q: Does Justif replace CSS text-align: justify?
A: No. Keep text-align: justify as the initial and fallback rendering. The library enhances supported paragraphs whose computed alignment already uses justify or justify-all.
Q: Why does a paragraph keep its native browser layout?
A: The paragraph probably contains an unsupported writing mode, embedded object, block descendant, inline margin, preserved whitespace rule, or mixed text direction. Add data-justif-debug to the automatic script or provide an onSkip callback through the API.
Q: Should refresh() run after changing paragraph text?
A: No. refresh() remeasures the existing scan. Destroy the controller and call justify() again after changing text, inline markup, or computed text styles.
Q: Can Justif run inside React or Vue applications?
A: Yes. Call justify() after the component mounts and retain the returned controller. Run destroy() during component cleanup before the framework removes or replaces the managed content.
Q: Does Justif preserve copying and assistive technology semantics?
A: The rendered result remains inline HTML and retains normal paragraph semantics, links, emphasis, selection, and find-in-page behavior. Clipboard cleanup removes characters introduced only for layout while preserving author-written nonbreaking spaces.
Changelog:
v0.9.0 (08/18/2026)
- Bugfixes
v0.8.1 (08/14/2026)
- Update
v0.8.0 (08/13/2026)
- Paragraphs can now begin with a floated element. Text besides the float is justified to the available width.
- Which characters hang is now configurable.
- Uppercase and lowercase css text transformations no longer prevent justification.
- Resizing no longer briefly displays incorrect line breaks, especially beside drop caps.
- Hanging punctuation no longer shifts adjacent text out of alignment.
- Short final lines no longer wrap unnecessarily beside a float.
- Fixed-width spaces such as &emsp and &ensp now retain their original characters and widths.
- Fixed justification failures in lines combining hanging punctuation and nonstandard spaces.
- Paragraphs containing uncommon control or separator characters now safely use browser justification.
v0.7.1 (08/03/2026)
- Bugfixes
v0.7.0 (08/01/2026)
- By default, punctuation is now only hung off the line ends.
- Behavior change: protrusion: false no longer switches off hanging punctuation automatically.
- hangingPunctuation’s options are now “line-end-only”, “first-line-and-line-ends”, “all-line-edges”, or “none”.
- The drop-in script is now configurable via CSS (–justif-*).
- Optical margin alignment now dynamically measures each font’s letterforms instead of relying on a hard-coded table.
- Bugfixes.
v0.6.5 (07/28/2026)
- Long single-paragraph passages now reflow much faster, including the 4,400-word excerpt on the demo page.
- On-screen paragraphs now receive their final spacing before justify() returns, avoiding a brief overhang when pages enhance before first paint.







