This document describes Fit text: a rule a rich text frame carries saying how its text may be adjusted to fit the box, and in what order. The canvas applies it, in order, only as far as it must.
It exists for one case above all: a host that refills a template's text for every order. The text is different every time and the box is not.
Source of truth:
autofitonPageElementRichTextandrefit()in the canvas package atsrc/page.ts; the rule shape isautofit_rulesinsrc/story.ts; the schema isPageElement\Autofit.
element.autofit = {
enabled: true,
steps: [
{ lever: 'hyphenation' }, // free — usually first
{ lever: 'point_size', to: 85, unit: 'percent', step: 0.25 },
{ lever: 'leading', to: 90, unit: 'percent' },
{ lever: 'letter_spacing', to: -20, unit: 'em' },
],
};
Read it as a sentence: you may break words; then take the type down as far as 85% of what I set; then close the lines up to 90%; then take up to 20/1000 em out of the letters.
lever |
unit |
to means |
Notes |
|---|---|---|---|
hyphenation |
— | — | on or off; takes no extent at all |
point_size |
percent or pt |
no smaller than | pt is measured against the smallest authored size in the frame |
leading |
percent or pt |
no tighter than | pt against the smallest leading any line resolves to |
letter_spacing |
em |
take out up to this much | a negative delta in 1/1000 em |
word_spacing |
em |
take out up to this much | a negative delta in 1/1000 em |
step is the granularity in the lever's own unit, and is optional — point size defaults to a quarter
point, so a fit lands on a size a designer reads rather than on 8.4173 pt.
Horizontal glyph scale is not a lever. Unlike the five above it exists nowhere in the canvas or in print, so it would be a rule nothing could honour.
Each lever is exhausted before the next is reached, and the search stops as soon as the text fits. So break words → smaller type → tighter lines is a different instruction from the same three in any other order: a designer who would rather lose a little leading than a point of type says so by putting leading first.
A lever may be named once. Pulling the same one twice cannot find anything the first pull did not.
⚠️ Spacing is stated as a negative delta, never as a percent, and that is measured rather than stylistic: letter and word spacing default to zero on almost every run there is, so "tighten to 80%" would be 80% of nothing and the lever would silently do nothing. A delta works from whatever each run authored and preserves the differences a designer set between runs. A positive value would add space and make the overflow worse, so it is refused.
Nothing is written back. native_font_size and every run's point_size stay exactly as authored; the
fit is a set of multipliers applied while the text is composed.
element.autofit |
the authored rule — on the wire |
element.autofit_factor |
the fit's result — not on the wire, recomputed |
Three reasons, and the first is why the feature was asked for:
const authored = element.native_font_size; // what the designer set
const effective = authored * element.autofit_factor; // what it came out at
const back = element.authored_point_size(effective); // the inverse, exactly
⚠️ A control that reports the wrong one disagrees with the document. The editor's size field shows the authored size — it has to, because typing into it writes one, and a field that showed 7.5 and then stored 7.5 would shrink the type again on every visit. The effective figure belongs where the rule is authored, which is where the Fit text panel puts it.
refit() is called at three moments, and never per keystroke:
update_native_rect();set_text() is discrete and fits immediately;A host that builds or rewrites text through the API and wants the fit recomputed sooner can call
element.refit() itself. It returns the point-size factor now in force.
It will not clip. A frame whose levers are exhausted keeps its overflow, so
validate()'s text_overflow check goes on telling the truth — an order cannot
reach print quietly missing its last lines. autofit_exhausted reports the same frame as a warning and
says, in detail.spent, what was actually spent.
It will not fit a linked frame. A chain's text belongs to the chain root, so every probe would have to reflow the whole chain — and shrinking in the first frame pulls text back from the third. A chained frame is left unfitted rather than half-fitted.
It will not fight AutoSize. AutoSize grows the box to hold the text; this shrinks the text to hold the box. Run together they argue in a loop — the box grows, so the text fits at full size, so the factor returns to 1, so the box grows — and the frame's height would depend on which ran last. AutoSize wins, because the height is already its output.
⚠️ Both of those are reported, not silent: autofit_not_applied names the frame and says which of
the two it is. Without it a designer who set a rule on such a frame would see no fit, no overflow
explanation and no complaint.
The rule is gated by its own edit option, separate from the size:
{ text: { size: true, autofit: false } } // retype and resize the type, but the rule stays mine
A host grants you may retype this while keeping and it still shrinks the way I set it — the rule is
the designer's and the words are the reader's. viewer_v1_defaults() withholds autofit: everything
else it grants is filling a template in.
The canvas answers the question through can(element, 'text_autofit'), and the editor's Fit text panel
does not appear when it says no.
In the viewer (since 20261104.3, TYP-1411) the toolbar of a selected text frame offers a Fit text
on/off button when both hold: the frame has a rule (autofit.steps is not empty) and the role allows
it. The button switches autofit.enabled and keeps the designer's steps, in one undo step, and the frame
refits at once. The viewer never writes a rule, and a frame without one shows no button. The editor's View ▸ Preview as viewer shows the button too. A host hides it
with gEditorUI.hide('toolbar.autofit').
The canvas emits its composed layout — per line and per word: position, text, family, style, size,
width, letter spacing, word spacing, baseline — and document-generator consumes exactly that. The
fitted size is therefore already in what print reads, with no second implementation in PHP.
This is unusual: most geometry rules in this system are written twice, once here and once in the publisher. This one is not, because the fit is applied where the words are measured.
There is no dedicated host method in this version — the rule is a field. A host that writes it should bracket the change so it can be undone, and refit:
const doc = window.typograph.get_typograph_document();
const spread = doc.pages[doc.active_page_index];
const element = spread.getElementByUUID(uuid);
doc.set_undo();
element.autofit = {
enabled: true,
steps: [{ lever: 'point_size', to: 70, unit: 'percent', step: 0.25 }],
};
element.refit(); // the rule changed, so the fit has
doc.set_redo();
doc.redraw();
⚠️ refit() records nothing of its own, and must not: a document whose text merely re-fitted is not a
document the reader changed. What belongs in the undo history is the rule.
// Before sending an order: which frames only fit because the type was shrunk, and by how much?
const shrunk = [];
doc.pages.forEach((spread, container_index) => {
spread.page_elements.forEach(el => {
if (el.type !== 'rich' || !el.autofit?.enabled) return;
if (el.autofit_factor === 1) return; // fits at its own size
shrunk.push({
container_index,
name: el.name || el.uuid,
authored: el.native_font_size,
effective: +(el.native_font_size * el.autofit_factor).toFixed(2),
});
});
});
// And the frames where it was not enough — the library has already worked this out:
const exhausted = window.typograph.validate({ checks: ['autofit_exhausted'] });
element.autofit |
{enabled, steps[]} — the authored rule, on the wire |
element.autofit_factor |
the point-size result, derived, not on the wire |
element.refit() |
recompute now; returns the factor |
element.authored_point_size(effective) |
the inverse — what a control should show |
| Levers | hyphenation, point_size, leading, letter_spacing, word_spacing |
| Units | percent, pt, and em for spacing (a negative delta in 1/1000 em) |
| Runs on | resize, text change (per burst), document load |
| Never | clips text, fits a linked frame, or overrides AutoSize |
| Permission | text.autofit — separate from text.size |
| needs nothing; the emitted layout already carries the fitted values |