Skip to content
KONIGI Patterns

Dashboards / Organization and navigation / Semantic grouping

‹ 2 of 4 ›

Semantic grouping

People need related panels grouped together so they can find and compare them.

Updated September 10, 2026

Problem

Panels added over time can end up beside unrelated information, forcing people to search for measures that belong together.

Solution

Group panels by a clear organizing principle, such as subsystem, signal type, or audience. Choose the principle that fits the page’s main task and apply it consistently.

Give groups specific names that describe their contents. Keep related panels close enough to compare, and use spacing or headings to separate groups.

Review the grouping as content changes. A new panel should fit the task, not simply go into the nearest available section.

Use when

A page contains enough panels that people need meaningful sections to scan it.

Don’t use when

There are six panels. Grouping six things into three groups adds ceremony and reduces the information density of every label.

Trade-offs

Every grouping favors some questions over others. Existing sections can discourage rethinking an outdated layout. Inconsistent organization across similar dashboards adds learning work.

Checklist

  • What is the grouping axis, and is it applied consistently down the whole page?
  • Would a different axis serve the primary reader better?
  • Does every group have a label that says something specific?
  • Is there a group called Misc, Other or Metrics?
  • Does proximity anywhere imply a relationship that does not exist?
  • Are groups ordered by importance, or by when they were added?
  • Do the same groups appear in the same order across related dashboards?
  • Does a new panel have an obvious home, or does it default to the last group?
  • Are group boundaries visible without relying on colour?
  • Does the grouping still make sense after the last reorganisation?

Compare

Grafana provides rows as the grouping mechanism, so a group is a labelled collapsible band, and the label is the only thing carrying the semantics. Datadog offers group widgets that nest within a page and can carry their own status, so a group is an object with state rather than only a divider. Netdata derives grouping from the collectors themselves—CPU, memory, disks, network, per application—which means the structure is generated rather than curated and is consistent across every node by construction. Honeycomb has little of this, because a board is a set of queries rather than a spatial arrangement, and the grouping question moves to which queries belong on which board.

Panel grid expresses relationships through placement. Collapsible row lets groups close. Tabs as genres separates groups at page level.

Multi-page dashboard gives larger topics their own space. Dense small-multiple layout depends on clear grouping for scanning.

Semantic grouping anatomy The same twelve panels twice. First in an even unlabelled grid, where proximity claims relationships nobody intended. Then in three labelled clusters with space between them, so adjacency means something. The same twelve panels, two layouts Even grid, no labels Proximity still makes claims Traffic Errors Saturation Grouped, labelled, spaced 1 2 3 1 PROXIMITY People scan rather than read, and assume things near each other relate. An even grid isn't neutral. 2 THE LABEL Written last, which is why so many say nothing. A group called "Misc" is a confession. 3 ONE AXIS Subsystem, signal type or audience. Pick one. Pages drift into all three as people add sections.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

Panels that belong together by meaning, laid out so the grouping is visible without a box around everything. A heading and a gap do more work than a border, and cost less ink.

shadcn
npx shadcn@latest add separator card
Tokens
--card--muted-foreground--border--status-warn--status-critical

Traffic

Requests / s

1,240

Active sessions

8,912

Egress

210 MB/s

Errors

5xx rate

0.21%

4xx rate

2.4%

Failed jobs

3

Saturation

CPU

61%

Memory

78%

Queue depth

42

PanelGroups.tsxOne grouping axis per page, a labelled section per group, and a label that says nothing gets flagged.

import type { ReactNode } from "react";
import { Separator } from "@/components/ui/separator";
import { cn } from "@/lib/utils";

/** One axis per page. Subsystem, signal type or audience, chosen once, so a
 *  page cannot group by service at the top and by signal further down. */
export type Axis = "subsystem" | "signal" | "audience";

export type Group = {
  /** Specific. "Misc", "Other" and "Metrics" are confessions, and they are
   *  flagged rather than silently accepted. */
  label: string;
  /** Ordered by importance, never by when they were added. */
  panels: ReactNode[];
};

type Props = {
  axis: Axis;
  groups: Group[];
  columns?: 2 | 3 | 4;
};

const SAYS_NOTHING = /^(misc|miscellaneous|other|metrics|general|stuff)$/i;
const COLS = { 2: "grid-cols-2", 3: "grid-cols-3", 4: "grid-cols-4" };

export function PanelGroups({ axis, groups, columns = 3 }: Props) {
  return (
    <div className="flex flex-col gap-5" data-axis={axis}>
      {groups.map((g) => {
        const empty = SAYS_NOTHING.test(g.label.trim());
        return (
          // A heading and a gap do the work. No box around everything, and
          // the boundary is visible without colour.
          <section key={g.label} aria-label={g.label}>
            <h3
              className={cn("text-[11px] uppercase tracking-[0.1em]", empty ? "text-status-warn" : "text-muted-foreground")}
              title={empty ? `"${g.label}" says nothing about what is inside. Name the ${axis}.` : undefined}
            >
              {g.label}
            </h3>
            <Separator className="mb-2 mt-1.5" />
            {g.panels.length > 0 ? (
              <div className={cn("grid gap-2", COLS[columns])}>{g.panels}</div>
            ) : (
              <p className="text-[11px] text-muted-foreground">Nothing here yet.</p>
            )}
          </section>
        );
      })}
    </div>
  );
}

demo.tsxHow it is called: nine panels by signal type, traffic then errors then saturation.

import { Card } from "@/components/ui/card";
import { cn } from "@/lib/utils";
import { PanelGroups } from "./PanelGroups";

/**
 * Nine panels grouped by signal type. Traffic, errors, saturation, in that
 * order, because that is the order a person diagnosing reads them. One error
 * panel is firing and sits where a reader would look for it.
 */
const Stat = ({ label, value, critical }: { label: string; value: string; critical?: boolean }) => (
  <Card className={cn("p-3 shadow-none", critical && "border-status-critical")}>
    <p className="text-xs text-muted-foreground">{label}</p>
    <p className={cn("mt-1 text-lg font-semibold tabular-nums", critical && "text-status-critical")}>{value}</p>
  </Card>
);

export default function Demo() {
  return (
    <div className="rounded-lg border bg-card p-4">
      <PanelGroups
        axis="signal"
        groups={[
          {
            label: "Traffic",
            panels: [
              <Stat key="rps" label="Requests / s" value="1,240" />,
              <Stat key="sessions" label="Active sessions" value="8,912" />,
              <Stat key="bw" label="Egress" value="210 MB/s" />,
            ],
          },
          {
            label: "Errors",
            panels: [
              <Stat key="5xx" label="5xx rate" value="0.21%" />,
              <Stat key="4xx" label="4xx rate" value="2.4%" />,
              <Stat key="jobs" label="Failed jobs" value="3" critical />,
            ],
          },
          {
            label: "Saturation",
            panels: [
              <Stat key="cpu" label="CPU" value="61%" />,
              <Stat key="mem" label="Memory" value="78%" />,
              <Stat key="queue" label="Queue depth" value="42" />,
            ],
          },
        ]}
      />
    </div>
  );
}
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.

Home Assistant

Room-based card layouts combine household device states with controls.

Demo dashboard September 10, 2026 Home Assistant public demo (signed out) medium · light · desktop-web
Cards are grouped by rooms such as Living room, Kitchen, and Study. States are written in words, with icon color providing a second cue. Section headers add useful room summaries, such as temperature or meeting status. The page also contains controls, including lighting sliders and thermostat buttons, so people can act in the same place they check the state.
Show 2 more examples Hide the rest

Kibana

A query bar and filtering controls define the data shown in charts and document tables.

Dashboards / list September 10, 2026 Elastic demo environment (guest session) medium · light · desktop-web
The dashboard list has fourteen pages, with many similarly prefixed Kubernetes entries on the first page. Search helps people find a specific destination without browsing the full collection. Descriptions beneath titles provide useful context, while prefixes and tags group related entries. The list would also benefit from a clear indication of which dashboards are maintained or recommended.

Netdata

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

Metrics / System September 10, 2026 Netdata Agent v2.10.0-686-nightly (public registry node, signed out) dense · dark · desktop-web
The navigation reports 720 charts organized by the metrics collected from the system. The main page follows the same nested structure, giving broad coverage but little indication of which charts matter most to a particular task. Search helps navigate that volume. The header also distinguishes live and stale nodes, making missing updates visible instead of presenting every node as current.