Skip to content
KONIGI

Dashboards / Structure / Overview then detail

1 of 4

Overview then detail

The viewer needs to start wide and end narrow without losing their place.

Updated September 10, 2026

Problem

Something is wrong somewhere in a system too large to display at once. The viewer has to narrow from everything to one thing, and at no point should they have to remember what they just saw in order to keep going.

Solution

Build the estate as levels. A top view that fits on one screen and says where to look, then a level per step of narrowing, each one carrying forward what the last one established.

Shneiderman’s mantra is the whole design in seven words: overview first, zoom and filter, then details on demand. The order is not a suggestion. Landing someone in a detail view without an overview means they are inspecting a thing they have no reason to believe is the right thing.

The overview’s job is to route the viewer to the right detail page, which is a different job from showing everything. It is to make the abnormal findable. That means it should show few enough things to scan and should encode state rather than magnitude, because at this level the question is where, not how much.

Each level down should answer a question the level above raised, and should visibly be the same subject narrowed. The most common failure here is a level that changes subject: a fleet view of hosts leading to a page organised by service, so the viewer arrives somewhere that cannot answer what they came with.

The other requirement is a way back that preserves state. NN/g’s framing of progressive disclosure applies: the split has to be right, so that people only go deeper on the occasions they need to. If going back means re-entering the time range and the filters, viewers stop going deep, and the levels below the first become decoration.

Use when

The estate is large, has a natural hierarchy, and different roles enter at different levels. Fleet to node, org to team to service, region to store.

Don’t use when

There are six things. A hierarchy over a small set is ceremony, and the flat view is faster. Also avoid it when there is no natural containment—forcing a hierarchy onto a graph makes one arbitrary path privileged and every other question hard.

Trade-offs

Levels are a claim about how the world is organised, and the claim is only true for some questions. Cost of traversal is real: three clicks each with a page load is a different experience from three clicks that filter in place. Overviews tend to grow, because every team wants their thing on the front page, and an overview that grew is no longer scannable and no longer does its one job. And people who live at the detail level bookmark it and never see the overview, which means the summary nobody reads gradually stops being maintained.

Checklist

  • Does the overview fit one screen without scrolling, at the smallest viewport used?
  • Does it encode state rather than magnitude, so abnormal is findable?
  • Does each level answer a question the level above raised?
  • Is every level obviously the same subject, narrowed?
  • Does time range, filter and selection survive both directions of travel?
  • How many steps from landing to the thing someone acts on?
  • Can a viewer arrive mid-hierarchy from an alert and still orient?
  • Is there a visible indication of where you are in the hierarchy?
  • What does the overview do when everything is healthy—does it still earn its place?
  • Who maintains the overview, given that most daily users skip it?

Compare

Datadog runs the pattern across products rather than within one dashboard: host map to host to process to trace, each hop carrying tags forward so the scope narrows rather than resetting. Sentry makes the hierarchy the data model—project, issue, event, trace—so the levels are objects with URLs rather than dashboards someone assembled, and the back button behaves. Netdata goes fleet overview to node to chart, with the node level being the same dashboard scoped, which keeps the vocabulary identical at every level. Grafana has no built-in hierarchy at all: levels are separate dashboards wired together by data links, which is fully flexible and means the coherence of the path is entirely a matter of whether one person thought it through. Cloudflare Radar uses it as an index rather than a drill. Each panel summarises a different topic and its arrow opens that topic’s own page, so the second level is a new subject rather than a narrowed view of the first. The overview becomes a table of contents, which only works because no scope is being carried down.

Drill-down is the mechanism this structure is made of. Host map is a common top level. Sidebar and canvas is how the levels stay reachable without a full traversal. Ranked list is often the overview that actually works. Multi-page dashboard is the flatter alternative when the levels are parallel rather than nested.

Overview then detail anatomy Three levels left to right. A fleet view of thirty small cells with one marked, then one host's panels, then a single panel with its log lines. Each level narrows the same subject, and each carries a way back that keeps the time range and filters. Three levels, one subject Fleet Where to look Host · web-07 Which signal P99 · web-07 What happened 1 2 3 1 THE OVERVIEW Few enough things to scan, encoding state rather than magnitude. The question at this level is where, not how much. 2 SAME SUBJECT, NARROWED Each level answers a question the one above raised. A fleet view of hosts leading to a page organised by service changes the subject. 3 DETAILS ON DEMAND The end of the walk, not the start of it. If going back means re-entering the time range, nobody goes deep twice.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Start wide, end narrow, keep your place. The overview's job is to route the reader to the right detail page, which means it needs exactly enough per item to choose and nothing more.

npm
lucide-react
Tokens
--foreground--card--muted--muted-foreground--border--status-nominal--status-warn--status-critical--status-unknown

Last 30 minutes · region eu-west

Fleet

Host · web-07

P99 · web-07

now 1380ms

  1. 14:07:12 GET /checkout 2004ms 504 upstream timeout
  2. 14:07:12 pool exhausted: 32/32 connections to pg-primary
  3. 14:07:15 GET /checkout 1988ms 504 upstream timeout
  4. 14:07:19 retry storm from cart-svc, 3x baseline

Drilldown.tsxFleet, host, signal as three columns that stay on screen. The range is held once; each level narrows the one before.

import { useState } from "react";
import { Sparkline } from "../sparkline/Sparkline";
import { STATUS_META, type Status } from "../semantic-status-color/status";

/**
 * Three levels, one subject. The fleet says where to look, the host says which
 * signal, the signal says what happened. All three stay on screen, so going
 * back is a glance rather than a page load, and the time range is held once
 * at the top rather than re-entered at each level.
 */
export type Signal = { id: string; label: string; status: Status; series: number[]; unit: string };
export type Host = { id: string; status: Status; signals: Signal[] };
export type LogLine = { at: string; text: string };

const CELL: Record<Status, string> = {
  nominal: "bg-status-nominal",
  warn: "bg-status-warn",
  critical: "bg-status-critical",
  unknown: "bg-status-unknown",
};
const STROKE: Record<Status, string> = {
  nominal: "text-muted-foreground",
  warn: "text-status-warn",
  critical: "text-status-critical",
  unknown: "text-status-unknown",
};

const title = "border-b pb-2 text-[11px] uppercase tracking-wide text-muted-foreground";

export function Drilldown({ hosts, range, logsFor, defaultHost, defaultSignal, onNavigate }: {
  hosts: Host[];
  /** Shown once and carried through every level. */
  range: string;
  /** The end of the walk: the lines behind one signal on one host. */
  logsFor: (host: Host, signal: Signal) => LogLine[];
  defaultHost?: string;
  defaultSignal?: string;
  /** Where the viewer is, for the URL, so a bookmark lands at the same depth. */
  onNavigate?: (path: { host: string; signal?: string }) => void;
}) {
  const [hostId, setHostId] = useState(defaultHost ?? hosts[0].id);
  const host = hosts.find((h) => h.id === hostId) ?? hosts[0];
  const [signalId, setSignalId] = useState(defaultSignal ?? host.signals[0].id);
  const signal = host.signals.find((s) => s.id === signalId) ?? host.signals[0];

  const pickHost = (h: Host) => { setHostId(h.id); setSignalId(h.signals[0].id); onNavigate?.({ host: h.id }); };
  const pickSignal = (s: Signal) => { setSignalId(s.id); onNavigate?.({ host: host.id, signal: s.id }); };

  return (
    <div>
      <p className="mb-3 text-[11px] text-muted-foreground">{range}</p>
      <div className="grid gap-5 sm:grid-cols-3">
        <section className="rounded-lg border bg-card p-4">
          <p className={title}>Fleet</p>
          <div className="mt-3 grid grid-cols-5 gap-1.5" role="list">
            {hosts.map((h) => (
              <button key={h.id} type="button" role="listitem" onClick={() => pickHost(h)}
                aria-label={`${h.id}, ${STATUS_META[h.status].label}`} aria-current={h.id === host.id}
                className={`h-5 rounded-[2px] ${CELL[h.status]} ${h.id === host.id ? "ring-2 ring-foreground ring-offset-1" : ""}`} />
            ))}
          </div>
        </section>

        <section className="rounded-lg border bg-card p-4">
          <p className={title}>Host · {host.id}</p>
          <div className="mt-3 grid grid-cols-2 gap-2">
            {host.signals.map((s) => (
              <button key={s.id} type="button" onClick={() => pickSignal(s)} aria-current={s.id === signal.id}
                className={`rounded border p-1.5 text-left ${STROKE[s.status]} ${s.status === "critical" ? "border-status-critical" : ""} ${s.id === signal.id ? "bg-muted" : ""}`}>
                <span className="block text-[10px] text-muted-foreground">{s.label}</span>
                <Sparkline values={s.series} width={64} height={18} />
              </button>
            ))}
          </div>
        </section>

        <section className="rounded-lg border bg-card p-4">
          <p className={title}>{signal.label} · {host.id}</p>
          <div className={`mt-2 ${STROKE[signal.status]}`}>
            <Sparkline values={signal.series} width={160} height={40} />
          </div>
          <p className="mt-1 text-[10px] tabular-nums text-muted-foreground">
            now {signal.series[signal.series.length - 1]}{signal.unit}
          </p>
          <ol className="mt-2 space-y-0.5 font-mono text-[9px] leading-tight text-muted-foreground">
            {logsFor(host, signal).map((l) => <li key={l.at}>{l.at} {l.text}</li>)}
          </ol>
        </section>
      </div>
    </div>
  );
}

demo.tsxHow it is called: twenty hosts, web-07 critical on P99, the walk opened at its log lines.

import { Drilldown, type Host, type LogLine, type Signal } from "./Drilldown";

/**
 * Twenty web hosts, one of them critical on P99. The walk opens on web-07's
 * P99 panel; clicking any other cell or signal moves the two columns to the
 * right without touching the one on the left.
 */
const quiet = (id: string, label: string, series: number[], unit: string): Signal =>
  ({ id, label, status: "nominal", series, unit });

const baseline = (n: number): Signal[] => [
  quiet("cpu", "CPU", [31 + n, 34, 30, 36, 33, 35], "%"),
  quiet("mem", "Memory", [61, 62, 61, 63 + (n % 3), 62, 62], "%"),
  quiet("rps", "Requests", [410, 420, 405, 440, 430, 425], "/s"),
  quiet("p99", "P99", [240, 250, 235, 260, 245, 250], "ms"),
];

const HOSTS: Host[] = Array.from({ length: 20 }, (_, i) => {
  const id = `web-${String(i + 1).padStart(2, "0")}`;
  if (id !== "web-07") return { id, status: "nominal", signals: baseline(i) };
  return {
    id, status: "critical",
    signals: [
      quiet("cpu", "CPU", [34, 38, 36, 52, 48, 55], "%"),
      { id: "p99", label: "P99", status: "critical", series: [250, 300, 290, 640, 720, 1420, 1380], unit: "ms" },
      quiet("mem", "Memory", [62, 62, 63, 63, 64, 64], "%"),
      quiet("rps", "Requests", [420, 418, 425, 380, 350, 310], "/s"),
    ],
  };
});

const LOGS: Record<string, LogLine[]> = {
  "web-07/p99": [
    { at: "14:07:12", text: "GET /checkout 2004ms 504 upstream timeout" },
    { at: "14:07:12", text: "pool exhausted: 32/32 connections to pg-primary" },
    { at: "14:07:15", text: "GET /checkout 1988ms 504 upstream timeout" },
    { at: "14:07:19", text: "retry storm from cart-svc, 3x baseline" },
  ],
};

export default function Demo() {
  return (
    <Drilldown
      hosts={HOSTS}
      range="Last 30 minutes · region eu-west"
      defaultHost="web-07"
      defaultSignal="p99"
      logsFor={(h, s) => LOGS[`${h.id}/${s.id}`] ?? [{ at: "—", text: `no events for ${s.label} on ${h.id} in this range` }]}
      onNavigate={(path) => console.log("route to", path)}
    />
  );
}
What it renders. Identical markup in both panes, with only the token values changing.

Examples

Captures whose hotspots reference this pattern, grouped by product and dated. The dashed boxes are this pattern; hover any box for the note.

Cloudflare Radar

A public dashboard with no account, no filters worth the name, and an audience of journalists. Designed for people who will read one number and leave.

Cloudflare Radar — Worldwide Overview
Multi-page dashboard. Eleven sections behind one rail, with the open one expanded in place. Radar is a site of dashboards, not a dashboard. Compare periods. The dotted series is the previous seven days, drawn on the same axis rather than beside it. Delta indicator. Eight shares with their change under them, all of them fractions of a point, which tells you how still this data is. Overview then detail. Two summaries and an arrow each. The panel exists to tell you whether the full page is worth opening. Ratio and rate. Shares only, never counts, and both sides of the split are named so the denominator is never in doubt. Stacked composition. Four mitigation techniques to 100%. The bar is almost redundant next to the printed figures, which is the point. Ranked list. A top ten with no magnitudes at all. The rank is the whole finding.
Worldwide Overview September 11, 2026 Public site, signed out; no version string exposed medium · light · desktop-web
Radar is a dashboard for people who did not come to use a dashboard. The audience is journalists, researchers and the merely curious, nobody has an account, and the design follows from that in two ways worth copying. Two controls scope the whole page—where, and when—and neither is a filter in the sense the rest of this gallery means it; the only other selector on the page sits inside the traffic panel. Everything else that looks like a control is a link. Each panel is a standing summary of a section that has its own full page behind the arrow in its heading, so the overview works as a table of contents rather than a filtered view of one dataset. And almost every value is printed as text above the chart that encodes it: "Bot 57.9%, Human 42.1%" sits over the bar rather than inside it. You can read the number without reading the chart, which is the right trade when most of your readers will take one figure and leave.
  • Multi-page dashboard Eleven sections behind one rail, with the open one expanded in place. Radar is a site of dashboards, not a dashboard.
  • Compare periods The dotted series is the previous seven days, drawn on the same axis rather than beside it.
  • Overview then detail Two summaries and an arrow each. The panel exists to tell you whether the full page is worth opening.
  • Ratio and rate Shares only, never counts, and both sides of the split are named so the denominator is never in doubt.
  • Stacked composition Four mitigation techniques to 100%. The bar is almost redundant next to the printed figures, which is the point.
  • Ranked list A top ten with no magnitudes at all. The rank is the whole finding.
  • Delta indicator Eight shares with their change under them, all of them fractions of a point, which tells you how still this data is.
Show 3 more examples Hide the rest

Honeycomb

Query-first; heatmaps and BubbleUp replace the dashboard-of-panels model with draw-a-region cross-filtering.

Honeycomb — Trace / cart checkout
Trace waterfall. Indentation is causality, length is duration, horizontal position is when it started. Six levels deep here. Trace waterfall. The staircase: nineteen SELECTs one after another inside getDiscounts. Batch the query, don't add a machine. Detail on demand. Selecting a span fills the right pane with its fields. The waterfall never moves while you read. Overview then detail. A minimap of all 71 spans above the list, so the shape of the whole trace is visible before you scroll it. Categorical series palette. Five services, five hues, and the name in a column beside every one. Colour is never carrying it alone. Percentile summary. This span's duration against the whole distribution, with this trace marked—so you know if you're looking at the tail.
Trace / cart checkout September 11, 2026 Honeycomb sandbox, public dataset (signed out) dense · light · desktop-web
Seventy-one spans over 3.288 seconds for one checkout, and the shape gives the answer away before you read a single duration. Two thirds of the way down, getDiscounts runs for 2.576s—more than three quarters of the whole request —and underneath it nine visible SELECT spans step down and to the right in a staircase, each starting after the last one finished. The badge on the parent says 19. Nineteen queries in a loop, run one at a time, and the waterfall says so by its outline rather than by any number. That is the shape worth learning: siblings overlapping means concurrency, siblings in a staircase means something that should have been one query. The panel top right is the other good idea here—it plots the distribution of this span's duration across the whole dataset and marks where this particular trace fell, so you can see whether you are looking at a normal request or the tail before you start optimising.
  • Trace waterfall Indentation is causality, length is duration, horizontal position is when it started. Six levels deep here.
  • Trace waterfall The staircase: nineteen SELECTs one after another inside getDiscounts. Batch the query, don't add a machine.
  • Percentile summary This span's duration against the whole distribution, with this trace marked—so you know if you're looking at the tail.
  • Detail on demand Selecting a span fills the right pane with its fields. The waterfall never moves while you read.
  • Overview then detail A minimap of all 71 spans above the list, so the shape of the whole trace is visible before you scroll it.
  • Categorical series palette Five services, five hues, and the name in a column beside every one. Colour is never carrying it alone.

Netdata

Per-second charts, hundreds per node, with a per-chart anomaly ribbon instead of a band on the series.

Metrics / System › Compute › CPU September 10, 2026 Netdata Agent (public registry node, signed out) dense · dark · desktop-web
Further down the same generated page, and the section nesting is the thing to look at: "− Compute", then "−− CPU", then "−− Pressure Stall Information (PSI)", then "−−− CPU", then "−−−− Some Pressure". Four levels, each collapsible, each named by the collector rather than by a person, and the depth is signalled with leading dashes because there was no design pass to give them a hierarchy in type. It is consistent and completely unedited. Every chart also carries its own query builder in the header—group by, aggregation, node count, dimension count, sample interval—so a panel here is a live query you can re-scope in place rather than a saved configuration. Worth noting what is not in this shot: the sidebar has an Anomaly Rate toggle, switched off, and Netdata's answer to the anomaly problem is a separate derived rate per chart rather than a band drawn around the series. This capture shows the control, not the thing it draws.
  • Collapsible row Four levels of section, depth carried by leading dashes. Generated by the collector, and never edited.
  • Gauge and dial Six arcs. Two are percentages where the endpoints mean something; four are rates in KiB/s and kbit/s on an invented maximum.
  • Legend and series toggle The legend is a value table: steal 0.1, softirq 0, user 1.5113, system 0.9, iowait 0.1, each with its own bar.
  • Dashboard builder A query builder in every chart header, so the panel is a live query rather than a saved configuration.
  • Overview then detail System › Compute › CPU. The breadcrumb is the only thing telling you where in 720 charts you are.

Plausible Analytics

One column, top to bottom, where the metric row doubles as the chart's control. The clearest working argument that a dashboard can have exactly one interaction.

Plausible Analytics — Live demo / plausible.io
Single-column narrative. One column, top to bottom, no panel arrangement and nothing to configure before reading starts. Overview then detail. The decomposition: sources, pages, geography, browsers, goals. Same subject, narrowed, no navigation. Ranked list. Sorted with an in-row bar, no share of total and no other row. Direct 271k versus Google 25.1k, out of what? Geo map with markers. A choropleth pale enough that every country but one reads as the same white. Raw counts, unnormalised. Header KPI strip. Six tiles sharing one anatomy, each with a delta. The boxed one is selected, and the chart below plots it. Ratio and rate. Bounce rate 43%, with the denominator two tiles away and the window only in the header. Tabs as genres. Tabs inside the panel—channels, sources, campaigns—so one card answers three questions in one slot.
Live demo / plausible.io September 10, 2026 Plausible live demo, plausible.io's own stats (signed out) medium · light · desktop-web
My single-column-narrative entry names Plausible as the reference implementation and says the trick is that the metric row doubles as the chart's control. Here it is doing exactly that: six tiles across the top, the first one boxed because it's selected, and the chart underneath plotting that metric and no other. Click a different tile and the chart follows. The page therefore has one interaction, and it is the same gesture as paying attention. Everything below reads as a single column in argument order—headline, then the shape behind it, then what it decomposes into, then goals. Two things it doesn't do. The ranked lists carry a count and a bar and no share of total, so Direct at 271k against Google at 25.1k tells you the ordering and not whether the top row is most of the traffic. And the choropleth is so pale that outside the United States almost every country is the same near-white, which is the encoding spending a whole panel to say "mostly America".
  • Single-column narrative One column, top to bottom, no panel arrangement and nothing to configure before reading starts.
  • Header KPI strip Six tiles sharing one anatomy, each with a delta. The boxed one is selected, and the chart below plots it.
  • Ratio and rate Bounce rate 43%, with the denominator two tiles away and the window only in the header.
  • Overview then detail The decomposition: sources, pages, geography, browsers, goals. Same subject, narrowed, no navigation.
  • Ranked list Sorted with an in-row bar, no share of total and no other row. Direct 271k versus Google 25.1k, out of what?
  • Geo map with markers A choropleth pale enough that every country but one reads as the same white. Raw counts, unnormalised.
  • Tabs as genres Tabs inside the panel—channels, sources, campaigns—so one card answers three questions in one slot.