Posted September 22nd 2026.
Written by Kristian Hansen.
Introduction
In my previous life, I was a Graphic Designer. I miss a few things from back then, and one of them is beautiful fully justified text columns. When Variable Fonts started emerging, I quickly realised some geniuses would probably allow us to combine variable font widths with letter-spacing and word-spacing to create more optically even text columns on web. Like a lot of other really cool frontend ideas, we’re still waiting (and unfortunately for good reason).
Now with the emergence of agent assisted coding, I figured this could be a neat demo. Proof of concept is probably too strong a wording, since I’m pretty sure I just proved to myself that there is, in fact, currently not a good way of doing this. So think of this more as a concept of a plan. Anyway, like someone else, I digress.
This is neat! A beautiful text column in the browser! Before getting into how this works, and my learnings, here are the disclaimers:
The Disclaimers
The demo requires a fixed column width. It can be adaptive, but it is not responsive.
The true holy grail would be an integration of native hyphenation, which proved to be a dead-end for a lightweight browser-based component.
Lots of manually tweaked optical trickery is happening under the hood.
It strictly requires a variable font with an active width
axis. In this demo, I'm using Roboto Flex.
The Demo
Custom text column
Browser default
How it works
To achieve improved optical justification without the horrific word-spacing 'rivers' of standard CSS text-align: justify, the component runs a multi-step programmatic pipeline inside Framer:
Optical Calibration
The core idea is to take a ragged-right text block and manipulate each line to fill a clean, fully justified 600px column. But because the engine compresses the font width as part of the process, the resulting lines naturally end up a bit smaller than 600px. Long story short, this causes a suboptimal word count per line. To counteract this in a very hands-on demo way, I simply increase the initial container width by 3 to 5 percent. This forces a natural, denser word count before the compression ever happens.The Baseline Measurement Pass
The engine splits text into lines, measures their unscaled widths, and calculates a target median width across the paragraph body (excluding the trailing line).Dynamic Multi-Axis Scaling
We then use the variable font'swdthproperty to stretch or compress each line toward the paragraph median. However, we don't stretch the font to the full column width; we leave a little bit of room on the shortest lines so letter-spacing and word-spacing can do some of the lifting too. To keep things from looking completely broken, we also enforce strict min and max ceilings on the width axis—ensuring letterforms never get squished into unreadability or stretched into grotesque proportions.Hybrid Spacing Weighting
Residual spacing deficits are distributed intelligently using a weighted ratio (favoring word spacing over letter spacing) to protect the structural integrity of the letterforms.
Learnings
Building this exposed some brutal realities about browser layout engines, alongside a few unexpected wins:
The Hyphenation Wall
Trying to extract native browser hyphenation dynamically via DOM ranges in a lightweight React component is a fragile dead-end due to iframe rendering inconsistencies. Without native hyphenation, extreme word-count disparities between lines can occasionally trigger texture clashes.Mobile Requires a Different Rulebook
In narrow columns, aggressive font stretching (wdth) creates heavy optical distortion because small line lengths cannot absorb wide letterform shifts. On mobile, stretching has to be dialed back significantly, letting subtle word and letter spacing do the heavy lifting.Editorial Curation Matters
Automated justification without layout feedback always fights against text. For a clean showcase, curating copy with a steady rhythmic word length makes all the difference.