Skip to content
KONIGI Patterns

Dashboards / Color / Grayscale with alerts

‹ 3 of 6 ›

Grayscale with alerts

People need warnings to stand out from routine information.

Updated September 10, 2026

Problem

When every chart uses strong colors, warnings have little chance to stand out.

Solution

Use neutral colors for normal conditions and reserve stronger color for states that need attention. Keep the meaning of each warning color consistent across the page.

Support warnings with labels, icons, or shapes so people do not have to distinguish color alone. Keep ordinary text and chart marks readable even when they are visually quiet.

Review the rules that trigger emphasis. A calm-looking display is useful only when missing data and unrecognized failures cannot silently appear normal.

Use when

People monitor a mostly normal system and need to notice exceptions quickly.

Don’t use when

The page is analytical and every series is equally interesting. Exploration needs categorical distinction, and a grey exploratory chart is just hard to read. Comparison work wants a palette; monitoring wants a resting state.

Trade-offs

Neutral colors make multiple series harder to distinguish. Incorrect thresholds can leave a problem unmarked. During a widespread incident, many warnings appear at once and need another way to show priority.

Checklist

  • What fraction of a healthy screen carries colour, and is it close to none?
  • Does saturation encode severity, or is every abnormal state equally loud?
  • Is anything coloured for a reason other than abnormality—branding, series identity, decoration?
  • Does the abnormal state have a non-colour signal too, per WCAG?
  • Which thresholds turn colour on, and when were they last reviewed?
  • What does the page look like when six things are wrong at once?
  • Do greys carry enough separation to still show structure and hierarchy?
  • Does the scheme survive a projector, a cheap wall TV, and a colourblind viewer?
  • Is there a categorical palette hiding in a chart library default somewhere on this page?
  • Who has authority to say no to the next request for a coloured element?

Compare

Industrial HMI software built to ISA-101 is where the discipline is enforced rather than encouraged, because a control room has a regulator and a fatigued operator and neither tolerates decoration. Netdata goes the opposite way by default, colouring densely across hundreds of charts, which suits a tool used for investigation rather than for standing watch. Grafana ships categorical palettes on by default and puts the whole burden on the dashboard author, so the muted approach is achievable and almost never the path of least resistance. Datadog sits in between with monitor status driving colour on status-oriented views while general dashboards stay fully coloured, which is coherent per-view and inconsistent across a product someone uses all day.

Semantic status color defines alert meanings. Dense small-multiple layout benefits from restrained emphasis. Categorical series palette handles distinct categories when identity matters.

Process mimic applies this approach to equipment displays. Wallboard mode supports ongoing monitoring.

Grayscale with alerts anatomy Two copies of the same six-panel screen. In the first, five panels are neutral grey, one is faintly marked as an early-stage state and one is fully marked as an alarm. In the second, every panel is marked, so nothing stands out. The same screen, two colour budgets Nine-tenths neutral Every team got their colour 1 2 3 4 1 NORMAL IS GREY Base graphics, chrome and every healthy series in neutrals. Roughly nine-tenths of the screen. 2 SATURATION IS SEVERITY Muted colour for an early-stage state. Full saturation held back for the severe ones. 3 NEVER COLOUR ALONE The alarmed panel also gets a border and a glyph. A tint on its own is a coin flip. 4 THE BUDGET Every exception is individually reasonable and collectively fatal. Attention is fixed.
Wireframe — the pattern's anatomy, not any one product's version of it

Implementation

A page where everything is coloured has no way to say "look here". Colour is spent, not applied, and this is the budget: the chrome is grey and the only saturated thing on screen is the thing that needs a person.

npm
lucide-react
Tokens
--card--foreground--muted--muted-foreground--border--status-warn--status-critical
prodeu-west15m

QuietWall.tsxNeutral unless a panel's status says otherwise. Warn is muted, critical is saturated and thicker, and both carry a border and the status glyph.

import { cn } from "@/lib/utils";
import { STATUS_META, type Status } from "../semantic-status-color/status";

/**
 * A monitoring wall that is grey until something is wrong.
 *
 * Chrome, labels and every healthy series are neutral. Colour is spent only
 * on a panel whose status is not nominal, and saturation carries severity:
 * warn is muted, critical is full strength and thicker. A coloured panel also
 * gets a border and the status glyph, so the state survives a projector and
 * a colourblind viewer, and colour is never the only thing saying it.
 *
 * Which status a panel holds is the caller's decision. The thresholds live
 * with whoever owns the rule, not in the drawing.
 */
export type Panel = { key: string; values: number[]; status: Status };

type Props = {
  panels: Panel[];
  unit: string;
  columns?: number;
  onOpen?: (panel: Panel) => void;
};

const STROKE: Record<Status, string> = {
  nominal: "stroke-muted-foreground",
  warn: "stroke-status-warn opacity-60",
  critical: "stroke-status-critical",
  unknown: "stroke-muted-foreground opacity-40",
};

const FRAME: Record<Status, string> = {
  nominal: "border-border",
  warn: "border-status-warn/50",
  critical: "border-status-critical",
  unknown: "border-dashed border-border",
};

const W = 84, H = 40;

export function QuietWall({ panels, unit, columns = 3, onOpen }: Props) {
  // One scale for the wall, so a climb is a climb.
  const all = panels.flatMap((p) => p.values);
  const lo = Math.min(...all), hi = Math.max(...all), span = hi - lo || 1;
  const n = panels[0]?.values.length ?? 1;
  const x = (i: number) => 6 + (i / (n - 1)) * (W - 12);
  const y = (v: number) => H - 4 - ((v - lo) / span) * (H - 8);

  return (
    <div className="grid gap-2" style={{ gridTemplateColumns: `repeat(${columns}, minmax(0, 1fr))` }}>
      {panels.map((panel) => {
        const { label, Icon } = STATUS_META[panel.status];
        const loud = panel.status !== "nominal";
        const last = panel.values[panel.values.length - 1];
        const d = panel.values.map((v, i) => `${i ? "L" : "M"}${x(i).toFixed(1)} ${y(v).toFixed(1)}`).join(" ");
        return (
          <button
            key={panel.key}
            type="button"
            onClick={() => onOpen?.(panel)}
            aria-label={`${panel.key}, ${label}, ${last} ${unit}`}
            className={cn("relative rounded-[2px] border bg-muted p-1.5 text-left", FRAME[panel.status])}
          >
            <span className="block text-[9px] leading-none text-muted-foreground">{panel.key}</span>
            {loud && (
              <Icon
                className={cn("absolute right-1 top-1 size-3", panel.status === "critical" ? "text-status-critical" : "text-status-warn opacity-60")}
                aria-hidden="true"
              />
            )}
            <svg viewBox={`0 0 ${W} ${H}`} className="mt-1 w-full" aria-hidden="true">
              <path
                d={d}
                fill="none"
                strokeWidth={panel.status === "critical" ? 2 : 1.5}
                strokeLinejoin="round"
                className={STROKE[panel.status]}
              />
            </svg>
          </button>
        );
      })}
    </div>
  );
}

demo.tsxHow it is called: six services, four nominal, one warn, one critical. Statuses come from the alert rules.

import { QuietWall, type Panel } from "./QuietWall";

/**
 * Six services, errors per minute. Four are healthy and grey. payments has
 * crossed the warn threshold and is coloured but muted. checkout is critical
 * and is the only saturated thing on the screen. The statuses arrive from the
 * alert rules; the wall only draws them.
 */
const PANELS: Panel[] = [
  { key: "edge", values: [16, 22, 14, 28, 22], status: "nominal" },
  { key: "api", values: [20, 26, 18, 30, 24], status: "nominal" },
  { key: "auth", values: [22, 16, 24, 18, 26], status: "nominal" },
  { key: "queue", values: [16, 24, 18, 26, 20], status: "nominal" },
  { key: "payments", values: [18, 22, 28, 34, 38], status: "warn" },
  { key: "checkout", values: [14, 18, 32, 44, 48], status: "critical" },
];

export default function Demo() {
  return (
    <div className="w-[320px] rounded-lg border bg-card p-4">
      <div className="mb-3 flex gap-2 text-[10px] text-muted-foreground">
        <span className="rounded-[2px] bg-muted px-2 py-0.5 text-foreground">prod</span>
        <span className="rounded-[2px] bg-muted px-2 py-0.5">eu-west</span>
        <span className="rounded-[2px] bg-muted px-2 py-0.5">15m</span>
      </div>
      <QuietWall panels={PANELS} unit="errors/min" onOpen={(p) => { location.hash = `#service-${p.key}`; }} />
    </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.

Ignition

A process-monitoring demo uses simulated plant data and offers simple or realistic equipment rendering.

Water Treatment / Overview September 11, 2026 Ignition Perspective public demo, Water Treatment (signed out) dense · dark · desktop-web
The demo uses simulated plant data in a process diagram arranged from incoming water through treatment and storage. Values appear beside the relevant equipment. Green means running, while stopped filters are gray and explicitly labeled. This makes normal operation colorful, unlike a display that reserves color for warnings. Repeated filter panels and clear process connections help people compare equipment.
Show 2 more examples Hide the rest

Grafana

Examples of panel grids, shared selectors, stat tiles, and chart configuration.

Examples / Stats, light theme September 10, 2026 Grafana Play (signed out; ?theme=light) dense · light · desktop-web
This light-theme capture shows the same stat-panel options as the dark version. Several value colors become pale against white, especially pink and orange. Whole-tile fills retain a different relationship between text and background, while the faint sparkline areas are harder to see. The pair is useful for checking each treatment on both backgrounds rather than assuming a theme switch preserves readability.
Examples / Stats September 10, 2026 Grafana Play (signed out; no version string exposed) dense · dark · desktop-web
Grafana’s stat showcase compares value colors, background fills, layouts, and text options. Most examples emphasize presentation without adding a comparison value or clear reference point. The No text panel uses color alone, leaving cells without labels or numbers. These are useful demonstrations of settings, but a working dashboard needs enough context to explain each value and state.
  • KPI tile A label, value, unit, and sparkline appear without an explicit comparison.
  • Dark-first data palette The dark theme makes these value colors more prominent than in the light capture.
  • Sparkline Sparklines use individual scales, so row-to-row comparison needs care.
  • Semantic status color Whole-tile color strongly emphasizes every value in this group.
  • Grayscale with alerts Multiple strong colors compete for attention.
  • KPI tile Values appear without labels explaining what they measure.
  • Semantic status color The cells have no text, leaving color as the only visible cue.
  • Header KPI strip Consistent label and value positions make the tile row easier to scan.