One filter row on top
One filter row scopes every chart; refetches keep the last render.
From dataviz (references/interaction.md, palette.md).
Per-card filters vs one row
Change a card's range, then a preset or region.
A filter on every cardBad
Revenue
$0
Revenue over time
By region
| Region | Revenue | Share |
|---|
One row aboveGood
Revenue
$0
Revenue over time
By region
| Region | Revenue | Share |
|---|
<div role="group" aria-label="Dashboard filters">
<div role="group" aria-label="Date range">…presets…</div>
<select aria-label="Region">…</select>
</div>
const slice = query(filters);
stat.render(slice); chart.render(slice); table.render(slice);
Skeleton flash vs held frame
Pick a range or spam Refetch; raise the latency.
Skeleton every refetchBad
Revenue
$0
Orders
0
shifts: Previous render at 50%Good
Revenue
$0
Orders
0
shifts: .dash { transition: opacity 200ms var(--ease-out); }
.dash.is-stale { opacity: 0.5; }
const mine = ++seq;
dash.classList.add("is-stale");
const slice = await load(filters);
if (mine !== seq) return; // a newer click won
render(slice);
dash.classList.remove("is-stale");
Date picker: presets as rows
Open each picker, pick a preset, then a custom range.
Custom firstBad
Presets firstGood
.row[aria-pressed="true"] .check { opacity: 1; }
@media (hover: hover) and (pointer: fine) {
.row:hover { background: color-mix(in srgb, var(--ink) 5%, transparent); }
}
.pop-foot { border-top: 1px solid var(--line); }
Why it matters, pros and cons
Why it matters
Per-card ranges let one heading give three answers. One row states the slice once, and a held frame keeps the reader reading.
Pros
- Numbers agree by construction.
- Scope lives in one place and the URL.
- No layout shift on refetch.
Cons
- A chart needing its own range belongs elsewhere.
- First load still needs a sized placeholder.
- A failed refetch must say so, not stay dimmed.