Word clouds that adapt to their container

Word clouds are often embedded in dashboards, split views, and cards that change size as users drag panels, collapse sidebars, or rotate a phone. Layout coordinates are computed for a specific width × height — stretching an old result with CSS won't reflow words into a new aspect ratio. The fix is straightforward: measure the container, call layout() again, and redraw.

Re-layout, don't rescale. CSS transforms and percentage widths can shrink a finished cloud visually, but they can't pack more words into a wider panel or rearrange terms when height changes. Treat each meaningful size change as a fresh layout pass against the container's pixel dimensions.

Typical responsive flow:

  1. Measure. Read the container's content box — usually via ResizeObserver or getBoundingClientRect() after a window resize.
  2. Layout. Pass the new width and height to layout() with the same weighted items. Font bounds can be tuned per breakpoint if needed.
  3. Draw. Update the canvas backing store (or SVG viewBox) to match, then render the returned placements.
  4. Debounce. Resize events fire in bursts while a user drags. Wait until motion settles — often 150–300 ms — before kicking off an expensive layout.

Step by step

Step 1

Watch the container, not just the window

A window resize listener misses panel drags inside the page. ResizeObserver fires whenever the element you care about changes size — the right hook for grid columns, splitters, and flex children. Debounce the callback so layout runs once after the user stops dragging, not on every intermediate pixel.

watch-resize.js
export function watchContainerResize(el, onResize, debounceMs = 1000) {
  let timer = null;
  const ro = new ResizeObserver((entries) => {
    const { width, height } = entries[0].contentRect;
    clearTimeout(timer);
    timer = setTimeout(
      () => onResize(Math.floor(width), Math.floor(height)),
      debounceMs,
    );
  });
  ro.observe(el);
  return () => { ro.disconnect(); clearTimeout(timer); };
}
Step 2

Layout at the measured pixel size

Set canvas.width / canvas.height (or the SVG viewBox) to the same values you pass into layout(). Height can follow width with a fixed aspect ratio, track the container directly, or come from your design system — as long as layout and renderer agree on pixels.

render-at-size.js
async function renderCloud(w, h) {
  canvas.width  = w;
  canvas.height = h;

  const placements = await layout(items, {
    mode: "pretty",
    width: w,
    height: h,
    minFontSize: 14,
    maxFontSize: 72,
    padding: 2,
  });

  drawWordCloud(ctx, placements);
}
Step 3

Drop results from an older resize

Layout is async. If the user resizes again while a pass is still running, an earlier result can arrive late and flash the wrong frame. Bump a generation counter before each call and ignore placements that come back after a newer resize has started.

render-at-size.js continued
let generation = 0;

async function renderCloud(w, h) {
  const pass = ++generation;
  const placements = await layout(items, { width: w, height: h, /* … */ });
  if (pass !== generation) return;
  drawWordCloud(ctx, placements);
}

Try it yourself

Random datasets at 50 or 500 words. Drag the handle on the right edge of the panel to resize the container — layout re-runs after you stop dragging (1 s debounce).

Random datasets at 50 or 500 words. Drag the handle along the bottom edge to pull the canvas taller — layout re-runs after you stop dragging (1 s debounce).

resizable container

Container width: —. Presets tune minFontSize / maxFontSize so words stay readable at each scale (50 → 16–88px, 500 → 10–52px). Try narrowing to mobile widths — words reflow into the new bounds instead of clipping.

Container size: —. Presets tune minFontSize / maxFontSize so words stay readable at each scale (50 → 16–88px, 500 → 10–52px). Pull the bottom handle to grow the canvas height — words reflow into the taller bounds instead of clipping.