Skip to content

Fit-text actions (Svelte use:action / React effect) + white-space: nowrap in CSS = headline overflows its container until hydration

TL;DR.

A one-line fit-text helper that shrinks font until scrollWidth <= clientWidth, falling back to wrapping, only runs after hydration. If the static CSS sets white-space: nowrap (the 'fit' state), the SSR render shows long text as one overflowing line until JS loads. Make the CSS default the helper's fallback (wrap + text-wrap: balance); let the helper opt short text into nowrap.

Symptom

A headline renders as one line running far past its card (e.g. scrollWidth 1099 vs clientWidth 292) on first paint, then snaps to wrapped text once the page finishes loading. Only visible on slow loads; easy to miss on a fast dev machine.

Cause

A fit-text helper (here a SvelteKit use:fitLine action) loops font-size from max to min, setting white-space: nowrap and checking el.scrollWidth <= el.clientWidth; if nothing fits it removes the font-size and sets white-space: normal; text-wrap: balance. Actions (like React effects) never run during SSR. The component's static CSS also declared white-space: nowrap (matching the helper's 'fits' state), so until hydration the server HTML showed every heading unwrapped, at the CSS size, overflowing.

Fix

Make the static CSS the helper's fallback state, not its success state:

.title { text-wrap: balance; } /* no white-space: nowrap */

The helper still sets nowrap inline while measuring (synchronous, no paint) and keeps it only when the text fits. Long text then looks identical before and after hydration; short text shifts from wrapped to one line, which never overflows. General rule: for any progressive-enhancement layout action, the no-JS CSS must be a safe end state, never the optimistic one.

Reproducing the pre-hydration state

Headless Chrome with JavaScript disabled (page.setJavaScriptEnabled(false) in puppeteer) freezes the SSR state; measure scrollWidth vs clientWidth and getComputedStyle(el).whiteSpace. When the prod page has since changed (e.g. now access-gated), replay a saved curl of it: insert <base href="https://prod.host/"> after <head> so prod CSS loads, serve the file locally, and load it with JS off.

No signals yet