Not a redesign of the tiles we have. A framework for placing any tile — the six we ship and the ten we've sketched — so the dashboard is genuinely good at 720 px (half a 14″ laptop, slides open in the other half) and genuinely good at 2240 px (24″ iMac), and everything between. Scrolling is allowed. We are not building the “CEO won't scroll” single pane of glass.
The move is to stop treating the dashboard as a fixed 1fr 1fr grid capped at
52rem — the only lens that doesn't fill its width — and give each widget three declared
properties: a natural width, a set of faces, and fluid proportions.
Three orthogonal axes, below. The live stage is real: it uses container queries, so resize it and watch each
tile change face on its own.
The dashed box is the responsive context, not the window. On half a 14″ it's ~720 px even though your screen is wider — so everything keys off this box. Snap to a target, or drag the bottom-right corner to feel every width in between.
What you're seeing. A wall of bricks — squares, wide cards, tall columns, full-bleed strips —
on a fixed row rhythm, so dense packs them tight instead of leaving ragged gaps. As the stage widens
the grid grows more columns and the wide/strip bricks reflow (Axis 1); inside each brick the content trades
its compact face for default then expanded (Axis 2); gaps, padding and hero numbers
grow with the space (Axis 3). At 720 px it's a calm two-column scroll; at 2240 px a dense bento wall.
Turn on grid tracks to see the brick lattice.
Any responsive layout problem decomposes into where a thing goes, what it shows, and how big its parts are. Keep them independent and the system stays legible as we add widget #11, #12, #16.
Drop the fixed 1fr 1fr and the 52rem cap. Use the RAM track grid Bristlenose
already ships (responsive-grid.css): as many columns as fit, dense packing for the bento. The trick that
makes it pack cleanly instead of ragged: a fixed base row height — every widget is a
brick of columns × rows, and content fills the brick rather than setting its height.
auto-fill, not auto-fit — at 2240 px auto-fit over-stretches a
sparse row into a few giant tiles. Brick area ∝ importance, never data volume (the bento rule).
Guardrail tiers clamp only the column span so a wide brick never overflows narrow.
.dash {
grid-template-columns:
repeat(auto-fill,
minmax(min(220px,100%), 1fr));
grid-auto-rows: 78px; /* the brick rhythm */
grid-auto-flow: row dense;
}
.w.c2 { grid-column: span 2 } /* wide */
.w.r3 { grid-row: span 3 } /* tall */
.w.cfull { grid-column: 1/-1 } /* strip */
A widget reacts to its own container, not the window (a tile can be narrow in a wide dashboard). Container queries pick compact → default → expanded: more detail, more content, or both. Default-first: the no-query baseline is the middle face, so a browser without container queries lands on something usable — narrow strips detail, wide adds it.
.w { container-type: inline-size; }
/* baseline = default face */
.lvl-max { display:none; } /* wide adds */
@container (max-width:299px){ /* narrow strips */
.lvl-mid { display:none; }
}
@container (min-width:480px){
.lvl-max { display:revert; }
}
Gap, padding and hero numbers interpolate between the two anchors with clamp() +
container units. A bigger screen means bigger numbers and more air — the Utopia idea — instead of a fixed rig
marooned in an ocean of margin.
.hero {
font-size: clamp(1.5rem, 9cqi, 3.1rem);
}
.w { padding: clamp(.7rem, 2.2cqi, 1.15rem); }
/* cqi = 1% of the widget's own width */
The contract every widget signs so the next one slots in without a bespoke decision. Author a new tile by filling this row: pick a brick shape (columns × rows), then describe its three faces. No layout code. The shapes come from a small, deliberate kit — that constraint is what keeps the wall clean.
| Widget | Brick (c × r) | Reads as | Compact | Default | Expanded |
|---|---|---|---|---|---|
| 1 · Numbers + saturation | cfull × r2 | top band | numbers wrap | numbers + spark | + delta vs last |
| 2 · Who we spoke with | cfull × r4 | person-cards | badge · arc · # | + thumb · topics | + journey map |
| 3a · Sections | c1 × r3 | column | counts hidden | list + counts | — |
| 3b · Themes (longer → wider) | c2 × r3 | 2-col column | 1 col | 2 cols | — |
| 4 · Pro & contra | c2 × r3 | two columns | top 2 ± | top 3 ± | top 4 ± |
| 5 · Friction & co-occurrence | cfull × r3 | pair-row · split 1 : 2 | two matrices welded into one brick so dense can't separate the partners | ||
| 6 · Signals | cfull × r2 | count query | 1 card | 3 cards | 5 cards |
| 7 · Verbatim (quotes) | cfull × r3 | count query | 1 quote | 3 spread | 5 spread |
| 8 · Coverage (last) | c3 × r1 | bounded strip · max ≈720 | bar | + legend | + omitted list |
| your widget #11… | ? × ? | — | pick a shape from the kit + three faces — done | ||
The brick kit (base cell --u ≈ 220px wide × --rowH 78px tall):
square c1×r2 · wide c2×r2 · big c2×r3 · tall c1×r3/r4 · panel c3×r3 ·
strip cfull×r1 · band cfull×r3.
A brick never gets a fixed pixel size — only a shape. The grid resolves the pixels, and the fixed row
height is what lets dense pack the wall. The discipline: constrain the kit. Ten free-form
aspect ratios is the ragged mess you just fixed; seven named bricks tile forever.
Extra width? A brick answers one of three ways: stretch (full-bleed — the session band, the sentiment strip) · stop (a max useful width — coverage caps at ≈720 and lets the rest breathe) · multiply (a quantity query — the signal cards go 1 → 3 → 5, showing more of the same rather than a bigger one). Knowing which a widget wants is half the layout decision.
The framework above is an assembly of published ideas, not an invention. Each axis stands on named work — verified current as of 2026.
The umbrella idea: let content and available space negotiate layout; media queries become optional
refinements, not load-bearing. Mix fluid fr and fixed ch/px tracks;
size regions to min-content/fit-content.
Repeat-Auto-Minmax. repeat(auto-fill, minmax(min(X,100%), 1fr)) — one line, one column to
many, no media query. The min(X,100%) guard (Evan Minto) stops overflow when the container is
narrower than the floor. This is line 8 of our own responsive-grid.css.
Algorithmic primitives, each intrinsically responsive. The Switcher flips row→column at a
content threshold via a flex-basis: calc((30rem - 100%) * 999) trick — no breakpoint,
reacts to the element's own width. Quantity queries stack once items exceed N.
@container + units cqi/cqb. A widget reacts to its box, not the viewport —
the exact fix for “same tile, different slot.” shadcn's dashboard-01 and Observable's
resize() both drive their grids this way, not by media query.
Design the two extremes, let clamp(min, intercept + slope·vw, max) interpolate the middle as one
continuous curve. Type and space breathe together — density stays coherent at any width. Use cqi
when the thing should track its card, not the page.
Heterogeneous tiles, one idea each, area ∝ importance (not data volume — that's the cited mistake;
too many equal tiles collapses back to a plain card grid). span N + grid-auto-flow: dense
packs the gaps.
Reflow reorders by priority (Wroblewski's “layout shifter”), it doesn't just stack columns. Dense packing + source order floats important tiles up as the stage narrows. Frost's caveat: never sweep a primary element behind a “more.”
Axis 2More space buys more information, not bigger chrome. Expanded faces show all sessions, the full matrix, axis ticks. Tufte's caveat we honour: only render a small multiple when each panel is big enough to read — so the compact face shows four, not forty.
Axis 2grid-lanesThe display:masonry vs grid-template-rows:masonry war resolved to
display: grid-lanes — reuse grid columns, pack down the block axis, keep source
order. Our responsive-grid.css already @supports (display: grid-lanes). One engine ships
it in 2026; we progressively enhance, dense is the stable fallback.
Concrete floors worth stealing: Datadog won't render a timeseries under 4 of 12 columns and
goes 2×12 high-density on big monitors; Observable auto-reduces 4→2→1 columns with no authored breakpoints.
Validates our per-widget min floor. We take the CSS-only subset — no react-grid-layout
runtime — because our tiles have known natural sizes.
On the “single pane of glass.” It's a named anti-pattern, not a virtue — the observability crowd documents wall-display dashboards so crammed that “teams walked right past incidents… largely for show.” Anat Sifri's rule (UX Collective): the fix for a busy dashboard is to reduce the information, not the scrolling. A researcher works this surface while writing slides — the defensible shape is progressive: the top band answers “what happened,” scrolling earns depth. We keep density and allow the scroll.