Library / SDK
lyallcooper/justif avatar
lyallcooper/justif

Justif: TeX-Style Paragraph Layout for the Browser

Text justification for perfectionists

470 stars10 forksTypeScriptMIT

At a glance

What is it?
Justif is a JavaScript library that re-breaks justified paragraphs as a whole, using the Knuth-Plass algorithm, bundled hyphenation patterns and microtypography. It is a progressive enhancement, and its cost is first paint.
Who is it for?
Adopt Justif if you publish long-form justified prose on the web and the uneven spacing of native justification bothers you more than a slower first paint. Do not adopt it for short marketing copy, for text you never set to justify, or for pages where first contentful paint is the metric you are judged on.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository last received commits 12 days ago.
What is it written in?
Mainly TypeScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 11, 2026, and from our analysis. They are not legal advice.

Editorial analysis

The one-line-at-a-time problem Justif sets out to fix

Browsers justify greedily. They pick a break for the current line, then move on, and a break that looks fine locally can leave the next line too loose. The README names the visible symptoms: rivers of whitespace and a poor break near the end of the paragraph. Anyone who has set a narrow column of justified text has seen this. The usual fixes are to widen the measure, reduce the font size, or give up and use ragged-right text.

Justif targets a specific reader: someone publishing prose in HTML who wants print-quality setting and is willing to pay for it in page weight and paint time. The README calls it "text justification for perfectionists", and the framing is honest about the scope. This is not a general typography toolkit. It does one thing, and it does it by replacing the browser's line-breaking decision with a global one. If your text is not justified, Justif does nothing, because it only enhances elements whose computed text-align is justify or justify-all.

Knuth-Plass, hyphenation and what stays in the DOM

The core mechanism is the Knuth-Plass line-breaking algorithm, which evaluates a paragraph as a whole rather than line by line. That is the same approach TeX uses, and it is why the output differs from the browser's: the algorithm can accept a slightly worse break early on to avoid a much worse one later.

On top of that, Justif layers several microtypography techniques. It hyphenates using bundled TeX patterns. It can hang punctuation into the margin for a cleaner text edge. On variable fonts with a wdth axis it makes small per-line width adjustments, and it can apply small letter-spacing adjustments when needed. For CJK text it justifies between characters using Japanese kinsoku rules.

The important architectural point is what the result is: inline HTML. Links, emphasis, selection, copying, find-in-page and assistive technology keep normal paragraph semantics. That is a deliberate constraint, and it rules out the failure mode you get from canvas-based or SVG-based text layout, where the text stops being text. The repository layout backs this up: there is a src/ directory, a dist/ build produced through tsup.config.ts, and a test-e2e/ directory alongside a Playwright config, which is what you would expect for a library whose correctness depends on real browser layout.

Installing Justif and justifying your first paragraph

There are two entry points. The automatic one is a single script tag, and the README's quick start keeps native justification in your CSS and loads the script in the head. The integrity hash needs to be updated or omitted when you update versions, so if you copy this and later bump the version, remove the integrity attribute or recompute it.

html
<style>
  article p {
    text-align: justify;
  }
</style>

<script
  type="module"
  blocking="render"
  src="https://cdn.jsdelivr.net/npm/[email protected]/dist/auto.js"
  integrity="sha384-Vb3imj2Dz+GDKt+QWPv0OfksiGXnqOAruRF7Uxdw0WEejuoPzDt8+AjL9JzmkzDt"
  crossorigin="anonymous"
></script>

Once the DOM is ready, that script scans p, li, dd, blockquote and figcaption elements and enhances only those whose computed text-align is justify or justify-all. You should set the page language so the right hyphenation rules load, for example lang="en-US" on the html element. Unlabeled and generic English content falls back to American English, other bundled languages load on demand, and if a language is not bundled the text is still justified, just without automatic hyphenation. To narrow the scan, add a data-justif-selector attribute to the script tag, such as "article .prose p". Adding data-justif-debug logs why a paragraph kept native justification.

The second entry point is the npm package, for when you want to choose elements and the hyphenator yourself:

sh
npm install justif
js
import { justify } from "justif";
import { hyphenateEnUS } from "justif/hyphenate/en-us";

const controller = justify(document.querySelectorAll("article p"), {
  hyphenate: hyphenateEnUS,
});

justify() applies its initial layout before returning, so the paragraphs you pass in are set by the time the call completes. Await controller.ready only when you need to wait for relevant fonts to load or fail and for the resulting layout to finish. The controller exposes ready, refresh(), rescan(), destroy(), the selected paragraphs, and managed, which is the set of paragraphs it is still responsible for and excludes any it declined. There is also an unjustify(elements) function for restoring elements when you no longer have the original controller.

The first-paint trade-off and the content-visibility workaround

The blocking="render" attribute stops the browser from painting native justification before Justif runs. The README is direct about the cost: a slower first paint, because the browser waits for the script to download and execute. Omit the attribute if first-paint speed matters more than avoiding a visible change. That is a real decision, not a footnote, and it is the main reason a team would walk away from the automatic entry point.

There is a second gap. Browsers without support for render-blocking module scripts, currently Firefox according to the README's caniuse link, may briefly show native justification while the script loads. For languages whose hyphenation patterns load on demand, the first paint is justified without hyphens, and hyphenation arrives with the pattern file. So the first thing a reader sees is not necessarily the final setting.

The README suggests self-hosting the entire dist/ directory without changing its structure and serving it with long-lived caching, since it loads ahead of first paint and repeat visits should come from cache. For long pages it recommends keeping off-screen paragraphs out of layout work with content-visibility: auto and contain-intrinsic-size, noting that Justif keeps placeholder heights exact so scrollbars and anchors stay stable. That detail matters: a layout library that shifts anchor positions on a long article is worse than uneven spacing.

Where Justif is the wrong tool

Justif is a progressive enhancement, and the README is clear that unsupported paragraphs are left untouched and that with JavaScript disabled native rendering is unchanged. That is good design, but it also means the enhancement is not guaranteed. If a paragraph cannot be measured reliably, it keeps native justification, and the debug attribute exists precisely because you are expected to need to find out why.

The ordering constraint is the sharpest limitation. Justif needs to run after other scripts that modify on-page text, such as math rendering or code syntax highlighting. If your pipeline injects or rewrites text after Justif has laid out a paragraph, the layout is stale. The API reflects this: if paragraph content changes, the documented remedy is to call destroy() and run justify() again. That is not a cheap incremental update, and on a page with live-updating text it is the wrong shape of tool.

Content changes aside, there is a list of CSS properties that require rescan() rather than automatic handling: hyphens, the font, letter-spacing, line-height, text-indent, min-width and contain. rescan() re-reads author CSS and re-lays out only the paragraphs whose styling actually changed, and it reconsiders paragraphs it previously declined. That is more efficient than a full pass, but it still means the library is watching for a class of changes it cannot observe on its own. Finally, if your text is ragged-right, Justif has nothing to do. It only touches elements computed as justify or justify-all.

How this differs from CSS hyphens and text-wrap

The obvious alternative is to stay with the platform: text-align: justify plus hyphens: auto, letting the browser hyphenate and break. The difference in approach is the unit of decision. The browser decides one line at a time; Justif decides for the paragraph as a whole. That is why the README can claim more even spacing. It is also why the platform version costs nothing at first paint and Justif costs a render-blocking script.

If you only need to avoid a bad last line, CSS offers text-wrap: balance and text-wrap: pretty, which adjust how a block's lines are distributed without replacing the line breaker. Those are cheaper and need no JavaScript, but they do not give you TeX hyphenation patterns, punctuation hanging, or the variable-font width adjustments Justif performs. The trade is scope for weight.

There is a third option worth naming: server-side typesetting. If you control the build, you can set text with TeX and ship the result, which moves the cost off the client entirely. Justif exists for the case where the HTML is the source of truth and you want the browser to do the final layout. Pick based on where your text is authored, not on which output looks best in a screenshot.

Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-10, so this is a project that is being worked on. The release history shows v0.9.1 on 2026-08-20, v0.9.0 on 2026-08-17 and v0.8.1 on 2026-08-14, which is a rapid sequence of minor releases in a short window and a reminder that the version is still below 1.0. Expect the API surface to move.

The package is published under the MIT licence, and the package.json files field ships only dist and LICENSE, so what you install is the built output plus the licence text. MIT is permissive, but this is not legal advice; if you redistribute the bundled hyphenation patterns, check how they are licensed inside the package rather than assuming the MIT header covers everything.

Upgrade cost is concentrated in two places. The CDN snippet carries an integrity hash that must be updated or omitted when the version changes, so pinning the version and the hash together is the safe pattern. The API has a small number of moving parts, and the ones that matter for upgrades are the controller methods (ready, refresh, rescan, destroy), the unjustify helper, and the per-language hyphenation entry points exposed under ./hyphenate/* in the exports map. The CHANGELOG.md at the repository root is where the breaking changes between these 0.x releases would be recorded.

Editorial conclusion

Adopt Justif if you publish long-form justified prose on the web and the uneven spacing of native justification bothers you more than a slower first paint. Do not adopt it for short marketing copy, for text you never set to justify, or for pages where first contentful paint is the metric you are judged on. Before committing, verify one thing: load the CDN script with blocking="render" on a real page in your slowest target browser, and check whether the delay is acceptable. Then check a paragraph containing links and emphasis to confirm the inline structure survives, and set the correct lang attribute so hyphenation matches your language.

Frequently asked questions

What does Justif do that CSS text-align: justify does not?

The browser breaks one line at a time, while Justif uses the Knuth-Plass algorithm to evaluate the paragraph as a whole, and adds hyphenation from bundled TeX patterns plus punctuation hanging and small tracking or width adjustments. The README describes the result as more even spacing than the browser's built-in justification.

How do I install Justif?

Either load the automatic entry from a CDN with a script tag in the head, or run npm install justif and import justify plus a hyphenation module such as justif/hyphenate/en-us. The automatic script scans p, li, dd, blockquote and figcaption elements whose computed text-align is justify or justify-all.

Does Justif work when JavaScript is disabled?

Yes, in the sense that nothing breaks. The README states that when JavaScript is disabled, native rendering is unchanged, because Justif is a progressive enhancement layered on top of your existing HTML and CSS.

Why did a paragraph keep the browser's native justification after adding Justif?

Justif only enhances paragraphs whose computed text-align is justify or justify-all, and it leaves paragraphs it cannot measure reliably untouched. The README documents a data-justif-debug attribute on the script tag, or an onSkip callback in the JavaScript API, to log the reason.

Official sources

  1. License: MIT
  2. lyallcooper/justif on GitHub
  3. Project website
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/lyallcooper-justif.svg)](https://hysenlabs.com/projects/lyallcooper-justif)