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
RegionRevenueShare
One row aboveGood
Revenue

$0

Revenue over time
By region
RegionRevenueShare
<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: 0
Previous render at 50%Good
Revenue

$0

Orders

0

shifts: 0
.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.