Skip to content
KONIGI

Dashboards / Page layout / Single-column narrative

4 of 5

Single-column narrative

A non-expert needs one obvious reading order and no configuration.

Updated September 10, 2026

Problem

The reader is not an analyst. They opened this to find out how things are going, they will spend ninety seconds, and a grid of twenty panels asks them to decide where to look before they have learned anything.

Solution

One column, top to bottom, in the order the argument runs. A summary, then the main chart, then the breakdowns, then the detail. No configuration, no panel arrangement, no choices to make before reading starts.

The grid’s weakness is precisely the freedom it gives: a grid has no reading order, so the viewer supplies one, and a viewer without expertise supplies a bad one. A single column removes that decision. Position becomes sequence, and sequence can carry an argument—here is the headline, here is the shape behind it, here is what it decomposes into.

That structure has a real consequence for what belongs on the page. In a grid, a marginal panel costs a corner. In a column, it costs the reader’s place in the sequence, so the editorial bar is much higher and the page stays shorter. The constraint does the pruning that a grid never forces.

Plausible is the reference implementation for this in analytics, and the choice that makes it work is that the top metric row doubles as a control: selecting a metric changes what the main chart plots. That collapses configuration into reading, so the page has one interaction and it is the same gesture as paying attention.

The failure mode is length. A single column with thirty sections is a scroll, and a scroll has the same problem as a grid—no priority—with the added cost that nothing below the fold is ever seen.

Use when

The audience is non-expert or occasional, the page has a story rather than a set of independent readings, and someone can take responsibility for the order.

Don’t use when

Viewers are comparing across sections. Anything that needs two things visible at once fails in a column, and that is most investigation work. Also poor where different roles need different things, since a single sequence serves the first audience and inconveniences the rest.

Trade-offs

A column wastes horizontal space on wide screens, and the usual remedy—letting content stretch—makes charts too wide to read. It scales badly with content, degrading into a long scroll with no way to skip. It demands editorial authority that most dashboards do not have, because someone has to say no. And its strength for a naive reader is a weakness for an expert, who now scrolls past four things they did not need to reach the one they did.

Checklist

  • Is there a real argument here, and does the order carry it?
  • Does the first screen answer the question most readers arrive with?
  • What is the reading time, and does it match the audience’s attention?
  • How many sections before this becomes a scroll rather than a sequence?
  • Is anything below the fold that matters, and does anything signal it is there?
  • Can a repeat visitor skip to what they came for?
  • How wide do charts get on a large screen, and are they still readable?
  • Is there any configuration, and could it be removed?
  • Who owns the order, and what is the process for adding a section?
  • Would an expert user find this slower than a grid?

Compare

Plausible is the clearest example, and the design decision worth stealing is making the summary row a control so choosing a metric and reading it are the same action. Public status pages run the same shape for the same reason: an unknown reader with no context needs one obvious order and no options. Grafana can approximate it with full-width panels stacked vertically, which works and gives up nothing structural, since a grid one column wide is a column. Google Analytics is the counter-example most people have used, having moved steadily toward configurable multi-panel reports and, in doing so, toward requiring expertise its casual audience does not have.

Panel grid is the alternative this pattern is defined against. Header KPI strip is usually the first section. Mobile adaptation ends up here by necessity, since a phone layout is a single column whether or not anyone designed it. Dense small-multiple layout is the opposite extreme, for the opposite audience. Auto-insight is what a narrative page reaches for when it wants to explain rather than only present.

Single-column narrative anatomy One column read top to bottom: a metric row that doubles as the chart's control, the main chart, the breakdowns beneath it, then the detail table. Position carries the sequence, and the sequence carries the argument. One column, one reading order Visitors 42.1k Views 96.4k Bounce 38% Time 2m 4s 1 2 3 4 1 THE ROW IS THE CONTROL Selecting a metric changes what the chart below plots. That collapses configuration into reading: one gesture, and it's the same gesture as paying attention. 2 THE HEADLINE The shape behind the number in the row above it. Nothing else competes. 3 THE DECOMPOSITION What the headline breaks into. A grid has no reading order, so the viewer supplies one, and a non-expert supplies a bad one. 4 THE DETAIL Last, because it's the answer to a question the three sections above have raised. In a grid a marginal panel costs a corner. In a column it costs the reader's place.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

One obvious reading order and nothing to configure. Each section states its finding in a sentence above the chart, so a reader who does not read charts still leaves with the answer.

npm
recharts
Tokens
--card--card-foreground--muted--muted-foreground--border--chart-1

Sources

  • Google18,400
  • Direct12,900
  • Newsletter7,800

Devices

  • Mobile22,700
  • Desktop15,100
  • Tablet4,300
PageVisitors
/pricing9,340
/blog/launch-week7,120
/docs/getting-started5,860

NarrativeReport.tsxHeadline, shape, decomposition, detail, in that order. The metric row is the one control, and it drives the chart.

import { useState } from "react";
import { Line, LineChart, YAxis } from "recharts";
import { cn } from "@/lib/utils";

export type Metric = {
  key: string;
  label: string;
  /** The headline figure, already formatted: "42.1k", "38%", "2m 4s". */
  value: string;
  /** One point per day in the window. The chart plots whichever is selected. */
  series: number[];
};
export type Breakdown = { title: string; rows: { label: string; value: number }[] };
export type Detail = { columns: [string, string]; rows: [string, string][] };

type Props = {
  metrics: Metric[];
  breakdowns: Breakdown[];
  detail: Detail;
  /** Which metric the chart opens on. Defaults to the first. */
  defaultMetric?: string;
  onSelect?: (key: string) => void;
};

/**
 * One column in the order the argument runs: headline, shape, decomposition,
 * detail. The metric row is the only control on the page, and selecting a
 * metric is the same gesture as reading it.
 */
export function NarrativeReport({ metrics, breakdowns, detail, defaultMetric, onSelect }: Props) {
  const [selected, setSelected] = useState(defaultMetric ?? metrics[0]?.key);
  const metric = metrics.find((m) => m.key === selected) ?? metrics[0];
  const pick = (key: string) => { setSelected(key); onSelect?.(key); };

  return (
    <div className="mx-auto flex w-full max-w-[380px] flex-col gap-3">
      <div className="grid grid-cols-4 divide-x rounded-lg border bg-card" role="tablist" aria-label="Metric">
        {metrics.map((m) => (
          <button
            key={m.key}
            role="tab"
            aria-selected={m.key === metric.key}
            onClick={() => pick(m.key)}
            className={cn("px-3 py-2.5 text-left", m.key === metric.key && "shadow-[inset_0_-2px_0_hsl(var(--chart-1))]")}
          >
            <span className="block text-[10px] text-muted-foreground">{m.label}</span>
            <span className={cn("mt-0.5 block text-base tabular-nums", m.key === metric.key ? "text-card-foreground" : "text-muted-foreground")}>{m.value}</span>
          </button>
        ))}
      </div>

      <div className="overflow-x-auto rounded-lg border bg-muted p-3">
        <LineChart width={320} height={80} data={metric.series.map((value) => ({ value }))} margin={{ top: 4, right: 0, bottom: 0, left: 0 }}>
          <YAxis hide domain={["dataMin - 5", "dataMax + 5"]} />
          <Line dataKey="value" stroke="hsl(var(--chart-1))" strokeWidth={2} dot={false} isAnimationActive={false} />
        </LineChart>
      </div>

      <div className="grid grid-cols-2 gap-3">
        {breakdowns.map((b) => {
          const max = Math.max(...b.rows.map((r) => r.value), 1);
          return (
            <div key={b.title} className="rounded-lg border bg-muted p-3">
              <p className="text-[10px] uppercase tracking-wide text-muted-foreground">{b.title}</p>
              <ul className="mt-2 flex flex-col gap-1.5">
                {b.rows.map((r) => (
                  <li key={r.label} className="flex items-center gap-2 text-[10px]">
                    <span className="h-2 bg-chart-1" style={{ width: `${(r.value / max) * 60}%` }} aria-hidden="true" />
                    <span className="truncate text-card-foreground">{r.label}</span>
                    <span className="ml-auto tabular-nums text-muted-foreground">{r.value.toLocaleString("en-GB")}</span>
                  </li>
                ))}
              </ul>
            </div>
          );
        })}
      </div>

      <table className="w-full rounded-lg border bg-muted text-[10px]">
        <thead>
          <tr className="border-b text-left uppercase tracking-wide text-muted-foreground">
            <th className="px-3 py-1.5 font-normal">{detail.columns[0]}</th>
            <th className="px-3 py-1.5 text-right font-normal">{detail.columns[1]}</th>
          </tr>
        </thead>
        <tbody>
          {detail.rows.map(([a, b]) => (
            <tr key={a}>
              <td className="px-3 py-1 text-card-foreground">{a}</td>
              <td className="px-3 py-1 text-right tabular-nums text-muted-foreground">{b}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

demo.tsxHow it is called: four metrics over seven days, opened on Visitors, two breakdowns and a three-row detail.

import { NarrativeReport } from "./NarrativeReport";

/**
 * Seven days of a site. Visitors is selected, so the chart shows the week
 * that adds up to 42.1k; picking Bounce or Time redraws it.
 */
export default function Demo() {
  return (
    <NarrativeReport
      metrics={[
        { key: "visitors", label: "Visitors", value: "42.1k", series: [2070, 3450, 2760, 6900, 5870, 10000, 11040] },
        { key: "views", label: "Views", value: "96.4k", series: [4740, 7900, 6320, 15800, 13440, 22900, 25300] },
        { key: "bounce", label: "Bounce", value: "38%", series: [44, 41, 42, 37, 38, 36, 35] },
        { key: "time", label: "Time", value: "2m 4s", series: [110, 118, 115, 128, 124, 131, 134] },
      ]}
      breakdowns={[
        { title: "Sources", rows: [{ label: "Google", value: 18400 }, { label: "Direct", value: 12900 }, { label: "Newsletter", value: 7800 }] },
        { title: "Devices", rows: [{ label: "Mobile", value: 22700 }, { label: "Desktop", value: 15100 }, { label: "Tablet", value: 4300 }] },
      ]}
      detail={{
        columns: ["Page", "Visitors"],
        rows: [["/pricing", "9,340"], ["/blog/launch-week", "7,120"], ["/docs/getting-started", "5,860"]],
      }}
    />
  );
}
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.

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.