Skip to content

Materialized width on a content-sized text box can quantize below the captured value, forcing a wrap the source never had #30

Description

@41fred

What happened

When a text-bearing box's content-derived width is materialized as a fixed px value (planWidth fixed, compiler/src/generate/css.ts ~2641), the emission pipeline can quantize it below the captured value. For a box captured at its max-content width there is zero slack, so any downward quantization — however small — tips the text onto a second line the source never had. With the height also materialized and no overflow clip, the overflow line often paints outside the box's background, so a word silently disappears rather than visibly breaking.

The archetype is an absolutely-positioned badge/tooltip/floating label: out-of-flow, shrink-to-fit, single line, materialized because its width can't be re-derived from flow context.

Source (left) vs clone (right) — same font stack, same 12px/700 text; the clone's "popular" wrapped and painted white-on-white below the pill:

source clone
source badge: "Most popular" clone badge: "Most"

To reproduce

  • Surface: CLI
  • URL cloned: locally-served fixture (below) — no external site
  • Command: npm run clone -- http://localhost:8787/ --out=./repro
  • Options: defaults
<!DOCTYPE html>
<html lang="en">
<head><meta charset="utf-8"><style>
  body { font-family: -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; padding: 80px; }
  .card { position: relative; width: 280px; border: 1px solid #ddd; border-radius: 12px; padding: 28px; }
  .badge { position: absolute; top: -13px; left: 24px; background: #2f6df6; color: #fff;
           font-size: 12px; font-weight: 700; padding: 4px 12px; border-radius: 999px; margin: 0; }
</style></head>
<body><div class="card"><p class="badge">Most popular</p><h3>Agency</h3><p>$129/mo</p></div></body>
</html>

On macOS / Chromium headless shell 141 the badge's captured cs.width is 75.328125px (content-box; in a box-sizing:border-box variant it's 99.328125px border-box — same mechanism). Generated page.tsx emits w-[75.3px] — 0.028px narrower than the text's max-content width — and the clone renders two line boxes ("Most" / "popular"; verified via Range.getClientRects()). The exact fraction is font-metric dependent; the mechanism isn't: any captured fraction that quantizes down reproduces this.

Expected behavior

The badge renders on one line, as captured.

Root cause: three independent down-quantization sites

  1. snapLen (tailwind.ts:39) rounds to the nearest 0.1px — down for ~half of all fractions (75.328 → 75.3).
  2. snapLen's integer branch has a float edge: 99.1 - 99 = 0.0999999999… which is < 0.1, so 99.1px snaps to 99px — a 0.1px round-down even of an already one-decimal value.
  3. snapBase (tailwind.ts:602) snaps a width within 0.25px of a named scale step onto the step (96.2px → w-24 = 96px) — after prettifyBase, so it can undo any earlier upward correction.

The percent-width path already guards exactly this cliff — css.ts:1710 ("a nowrap pill … has NO slack: rounding the % DOWN emits a width below its intrinsic content") — and in-flow leaves get white-space:nowrap via nowrapWrapVulnerable (css.ts:3141). But that inference needs the sizing probe's wMin/wMax, and the probe is limited to static/relative nodes (walker.ts:367), so out-of-flow text boxes fall through every existing defense: no probe evidence → no width:auto recovery, no nowrap, and a materialized px width that quantizes with no slack.

Related paths with the same exposure (not demonstrated, from code reading): mixedFillByVp regimes that store raw cs.width px inside a percentVp plan (css.ts:1759 → emitted at css.ts:2632); generic flex-basis/max-width arbitrary lengths (css.ts:106,118); text-bearing pseudo-elements (css.ts:3322). The basis plan variant would also qualify but appears to have no producer at present.

Fix directions (we prototyped one; happy to PR whichever you prefer)

  • A. Extend the sizing probe to out-of-flow text boxes — probe width:auto on absolute/fixed leaves like the in-flow probe does, and when the browser proves auto reproduces the captured box, drop the materialized width entirely. Follows the existing browser-as-oracle sizingVerdict design, restores genuine shrink-to-fit (no quantization at all), and gives nowrapWrapVulnerable the wMin/wMax evidence to add nowrap where the box sits flush. Most principled; most work.
  • B. Directional (ceil) quantization for content-tight materialized widths. Prototyped: pre-ceiling cs.width to 0.1px for text-bearing nodes at the fixed branch fixes the repro (w-[4.7125rem] = 75.4px, one line) and passes the full suite (clone-static 525/525, core 13/13 incl. the Gate 6 determinism golden). But as-is it is not monotone through the pipeline — sites 2 and 3 above can still re-round down — and keying on "has a direct text child" is both under-inclusive (descendant text via a wrapper <span>) and over-inclusive (authored fixed widths don't need widening). A real version needs a monotonicity guarantee through snapLen + snapBase, content-tight evidence, and regression tests for the 99.05 → 99.1 → 99px and 96.11 → 96.2 → w-24 boundary cases.
  • C. white-space:nowrap on captured-single-line out-of-flow text leaves. Empirically fixes the badge even with the too-narrow width (0.03px invisible overflow instead of a wrap), and is robust to font-metric drift across platforms in a way sub-pixel width fixes are not. Needs the same care the in-flow inference applies, and A would supply the intrinsic-width evidence to do it on your existing terms.

Our read: A matches the codebase's design philosophy, with C as a natural byproduct; B alone is a band-aid whose edge cases are worse than they look.

Environment

  • OS + Node: macOS 15 (Darwin 25.5.0), node v24.14.1
  • Commit / version: ditto.site@0.1.0 at current main
  • Chromium installed: yes (Playwright Chromium headless shell 141)

Logs / artifacts

Measured on the styled (border-box) variant: source badge getBoundingClientRect().width = 99.328125, one line box; clone = 99.296875 (w-[99.3px]), two line boxes ("Most" 28px / "popular" 44px). The snapLen float edge and snapBase down-snap were verified by direct evaluation. Repro is a fabricated local fixture; no external site captured.

No activity

Activity on this issue will appear here.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions