Bristlenose · infoviz mockups · dashboard

Dashboard auto-layout — a way of thinking at all scales

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.

LIVE The stage — drag it, or snap to a target

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.

Target
Show
Zoom
720px · ~2 cols · 100%

The study at a glance

128quotes
9sessions
14themes
92%saturation

Who we spoke with

P1 Rachel42:10
onboardingpricing
OnboardingCore taskPricingWrap-up
18quotes
P2 Dan38:55
core tasktrust
OnboardingCore taskTrust & safetyWrap-up
21quotes
P3 Aiko51:30
sharingwrap-up
OnboardingCore taskSharingWrap-up
14quotes
P4 Ben29:08
core task
OnboardingCore taskWrap-up
27quotes
P5 Mara44:22
pricingsharing
OnboardingPricingSharingWrap-up
19quotes

Sections

  • Onboarding 21
  • Core task 44
  • Pricing 18
  • Trust & safety 27
  • Sharing 12
  • Wrap-up 18

Themes longer, so wider + 2 columns

  • Trust & saving 31
  • Findability 24
  • Speed 22
  • Onboarding friction 19
  • Pricing clarity 18
  • Undo & safety net 16
  • Collaboration 14
  • Export & sharing 12
  • Mobile parity 9
  • Notifications 7

Pro & contra

saved me time +9 clear layout +6 fast to learn +4 felt trustworthy +3
no undo −8 hidden filter −5 slow export −3 unclear pricing −3

Friction & co-occurrence

Friction section × sentiment

Co-occurrence theme × theme · 2× the width

Signals 1 · 3 · 5 by width

Trust & saving31
Findability24
Speed22
Onboarding friction19
Pricing clarity18

Verbatim 1 · 3 · 5, spread by width

“I kept waiting for it to tell me it had saved — I never trusted that it had.”P4 · frustration · 12:03
“Once I found the filter it was actually fine.”P7 · satisfaction · 08:41
“I’d share this with my team tomorrow if the export weren’t so slow.”P2 · doubt · 21:15
“The onboarding just got out of my way, which I loved.”P5 · delight · 03:52
“I couldn’t tell if pricing was per seat or per project.”P1 · confusion · 17:30

Transcript coverage

72% report · 15% moderator · 13% omitted

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.

MODEL Three orthogonal axes

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.

AXIS 1

Placement — the grid fills the lens

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 */
AXIS 2

Adaptation — each widget owns three faces

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; }
}
AXIS 3

Proportion — sizes breathe, they don't just gain whitespace

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 */

SPEC The widget size taxonomy

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.

WidgetBrick (c × r)Reads asCompactDefaultExpanded
1 · Numbers + saturationcfull × r2top bandnumbers wrapnumbers + spark+ delta vs last
2 · Who we spoke withcfull × r4person-cardsbadge · arc · #+ thumb · topics+ journey map
3a · Sectionsc1 × r3columncounts hiddenlist + counts
3b · Themes (longer → wider)c2 × r32-col column1 col2 cols
4 · Pro & contrac2 × r3two columnstop 2 ±top 3 ±top 4 ±
5 · Friction & co-occurrencecfull × r3pair-row · split 1 : 2two matrices welded into one brick so dense can't separate the partners
6 · Signalscfull × r2count query1 card3 cards5 cards
7 · Verbatim (quotes)cfull × r3count query1 quote3 spread5 spread
8 · Coverage (last)c3 × r1bounded strip · max ≈720bar+ 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.

SOURCES Prior art — who worked this out already

The framework above is an assembly of published ideas, not an invention. Each axis stands on named work — verified current as of 2026.

Intrinsic Web Design

Jen Simmons · 2018

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.

Axis 1

The RAM technique

Una Kravets · web.dev

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.

Axis 1

Every Layout

Heydon Pickering & Andy Bell

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.

Axis 1

Container Queries

CSS WG · Baseline 2023 · ~93% global

@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.

Axis 2

Utopia — fluid type & space

Trys Mudford & James Gilyead

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.

Axis 3

Bento layouts

Apple keynotes · iOS 16 · visionOS

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.

Axis 1

Content choreography · Priority+

Trent Walton · Luke Wroblewski · Brad Frost

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 2

Small multiples / data density

Edward Tufte

More 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 2

Native masonry → grid-lanes

CSS WG resolution · late 2025

The 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.

Axis 1

Grafana · Datadog · Observable

production dashboard grids

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.

Axis 1

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.