Skip to content
KONIGI Patterns

Dashboards / Organization and navigation / Overview then detail

1 of 4 ›

Overview then detail

People need to move from a broad overview to specific details without losing context.

Updated September 10, 2026

Problem

People need to narrow a large system down to a specific area without losing the context that led them there.

Solution

Start with a compact overview that helps people choose where to investigate. Link each item to a more specific view of the same subject.

Carry the selected time range and filters through each level. Show the current location and provide a way back that restores the earlier state.

Keep the overview focused on useful decisions rather than every available metric. Let people link directly to detailed views when they already know where they need to go.

Use when

The information has meaningful levels, and people need to move from a broad picture to specific records or components.

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

One hierarchy may not fit every question. Each step adds navigation time, and an overloaded overview loses its value. Direct links to detail also need enough context for people who skip the overview.

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 connects the levels. Host map can provide a fleet overview. Sidebar and canvas keeps destinations accessible.

Ranked list helps identify where to start. Multi-page dashboard separates peer questions instead of nested levels.

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

Screenshots are grouped by product and include the capture date. Dashed boxes highlight this pattern. Notes appear below each screenshot and when you hover over a box.

Cloudflare Radar

Public internet-traffic dashboards with location and time controls and links from summaries to detailed views.

Worldwide Overview September 11, 2026 Public site, signed out; no version string exposed medium · light · desktop-web
Radar’s overview uses location and time controls to set the scope of the page. Each summary panel links to a fuller section, making the page a starting point for further reading. Values are printed above the charts, so people can read figures without estimating them from marks. The bot and human shares, for example, are both labeled beside their bar.
Show 3 more examples Hide the rest

Honeycomb

Query-driven views use heatmaps and selected regions to investigate differences between requests.

Trace / cart checkout September 11, 2026 Honeycomb sandbox, public dataset (signed out) dense · light · desktop-web
The trace contains seventy-one spans across one checkout request. The getDiscounts operation occupies much of the total duration, with visible SELECT spans arranged sequentially beneath it. That sequence is a useful place to investigate repeated work, though the chart alone does not establish the right fix. A distribution panel places the selected span’s duration among other observations, helping people judge whether this request is typical.

Netdata

A large collection of system charts with per-chart controls and anomaly-rate views.

Metrics / System › Compute › CPU September 10, 2026 Netdata Agent (public registry node, signed out) dense · dark · desktop-web
This section follows several nested levels of the generated metric hierarchy. Leading dashes indicate depth, while a breadcrumb identifies the current CPU section. Each chart includes its own grouping and aggregation controls. The Anomaly Rate option is visible but switched off, so this capture demonstrates the control rather than the resulting anomaly display.

Plausible Analytics

A metric row controls the main chart, followed by focused traffic breakdowns.

Live demo / plausible.io September 10, 2026 Plausible live demo, plausible.io's own stats (signed out) medium · light · desktop-web
The metric row controls the main chart: the selected tile is outlined and its series appears below. Supporting sections then break the results down by source, page, geography, and other categories. Ranked lists show counts and bars, but no visible share of the total. The map uses very pale colors for most countries, limiting how much variation is easy to see.