Consigne

Building an industrial monitoring dashboard with React and shadcn/ui

A plant dashboard is not a SaaS dashboard with different numbers. It must stay grey while everything is normal, show each value against its limits, draw trends on a fixed scale, treat alarms as states the operator acknowledges, and never display a stale value as if it were live. shadcn/ui gives you the primitives; this guide covers the rules to add on top.

Published 27 October 2026 · 6 min · by Antoine Despres, Consigne

Why SaaS dashboard habits fail on the plant floor

Most React dashboard templates are built to impress: a green arrow for growth, a coloured card per metric, charts that rescale and animate on every update. In a control room these habits do harm. An operator watches the screen for a whole shift, and anything that draws the eye for no reason wears down attention. If the running pump is green and the full tank is blue, the red of a real alarm has to compete with the whole screen.

The high-performance HMI approach, framed by ANSI/ISA-101.01-2015, takes the opposite view: calm in normal operation, so the abnormal stands out at once. The sections below translate that idea into React components and shadcn/ui tokens.

Status colours: grey first, colour for the abnormal

shadcn/ui already themes everything through CSS variables (background, card, muted, border…). Keep those for the neutral surface and add a small set of tokens whose only purpose is abnormal states:

TokenUseNever use it for
alarm-1 to alarm-4alarm priorities, 1 critical to 4 lowdecoration, "success" states
qualityuncertain, bad or frozen dataprocess alarms
processrunning equipment, nominal values, as a dark neutralanything that must catch the eye
track, track-normalindicator track and normal band

Three rules make the scheme hold:

  • Running is not green. A running pump is normal; normal is grey. Equipment state is carried by shape and text (a filled or outlined symbol, a "Running" label), not by colour.
  • Quality has its own colour, different from every alarm colour, so a sensor fault is never mistaken for a process problem.
  • Colour is never alone. Each alarm badge carries its priority number and a label, plus an aria-label. Print the screen in greyscale: if you can no longer tell priorities apart, the coding is incomplete.

With Tailwind CSS 4, declare the tokens in @theme inline and define them under :root and .dark, as shadcn does for its own variables. A developer who wants a bright card "to make it look nice" then simply finds no token for it.

Values against their limits

"72.4 °C" does not say whether that is good. A measurement needs its context: range, LL/L/H/HH limits, setpoint. Model it once and pass it everywhere:

type Quality = "good" | "uncertain" | "bad" | "stale"

interface TagValue {
  tag: string             // "PT-101"
  label: string
  value: number | null    // null when missing, never a fake 0
  unit?: string
  quality?: Quality
  timestamp?: number      // source time, not browser time
  min?: number
  max?: number
  limits?: { ll?: number; l?: number; h?: number; hh?: number }
  setpoint?: number
}

Evaluate the limits in a fixed order, so that high-high wins over high:

function limitState(v: number | null, lim?: TagValue["limits"]) {
  if (v == null || !Number.isFinite(v) || !lim) return "normal"
  if (lim.hh !== undefined && v >= lim.hh) return "highhigh"
  if (lim.ll !== undefined && v <= lim.ll) return "lowlow"
  if (lim.h !== undefined && v >= lim.h) return "high"
  if (lim.l !== undefined && v <= lim.l) return "low"
  return "normal"
}

A missing value returns "normal" on purpose: the problem is shown by the quality indicator, not by a false threshold alarm. Validate limits at configuration time (LL ≤ L ≤ H ≤ HH) rather than at render time.

On screen, prefer a horizontal analog bar to a round gauge: a grey track, a subtly darker normal band, limit ticks and a pointer. Several bars stacked vertically let the eye spot the one value out of line in a second.

shadcn/ui charts are built on Recharts, which helps: ChartContainer and ChartTooltip wrap standard Recharts components. Three settings matter for process trends:

  • A fixed Y domain set by engineering, for example the instrument range. Autoscaling turns measurement noise into an apparent disaster. In Recharts, pass an explicit domain={[min, max]} to YAxis.
  • No animation. Set isAnimationActive={false} on each series. A trend that moves on every update makes the whole screen move.
  • Limits drawn on the chart. A ReferenceLine for H and HH, or a ReferenceArea for the normal band, shows how close the value is getting.

Avoid dual Y axes: two scales on one chart invite false correlations. Use small multiples with aligned time axes instead. Give the operator a pause button: a trend that keeps scrolling while someone tries to read a point is frustrating.

Alarms: states, not notifications

An alarm is not a toast. ISA-18.2 treats it as a state with two dimensions, active or not and acknowledged or not:

ActiveAcknowledgedMeaningDisplay
yesnonew, nobody has seen itpriority colour, slow blink
yesyesseen, the cause is still therepriority colour, steady
nonocleared before anyone saw itstays in the list until acknowledged
noyesfinishedleaves the list, remains in the event log

Sort the list by state first (unacknowledged before acknowledged), then by priority, then by time. Acknowledgement must record who and when. Shelving (hiding an alarm until a set time) must be visible and time-limited, never a silent mute.

Measure the alarm rate as well. Figures commonly quoted from ISA-18.2 for one operator: about 1 alarm per 10 minutes on average is very likely acceptable, about 2 is the maximum manageable. A small "alarms per 10 minutes" block and a list of the most frequent sources (chattering alarms) tell the engineering team where to act.

Data quality: never lie about a value

A web dashboard adds failure modes a classic SCADA handles for you: a dropped WebSocket that leaves the last value on screen, a null rendered as 0, a NaN displayed as is. A value frozen by a lost link looks exactly like a stable one.

  • Carry quality and a source timestamp with every value. The OPC UA status families Good, Uncertain and Bad map directly onto the first three states.
  • Detect stale data: if the timestamp has not moved for longer than the expected update period, mark the value as frozen, even if the connection claims to be open.
  • Monitor the link with a heartbeat, not only with the age of the last value.
  • Render a missing value as a dash, with the quality colour and a short reason, never as zero.

Real-time blocks that compute ages ("3 min ago") need a shared clock. Pass a now value from one hook to every block instead of calling Date.now() during render; it also avoids hydration warnings with Next.js server rendering.

Commands: hand the action back, show the result

Display components should not write to a PLC themselves. Expose callbacks (onAck, onApply, onCommand) and let your server or gateway perform the write with its own access control. In the interface, every command takes two steps (select, then confirm in a dialog that states what will happen), a setpoint is bounded before it is sent, and the screen shows the state returned by the PLC, not the command sent.

Ready-made blocks

The shadcn/ui industrial blocks apply these rules as 31 React blocks and 3 full screens (fleet, site, machine): threshold KPI cards, trends with limits, an ISA-18.2 alarm table with acknowledgement and shelving, alarm rate per 10 minutes, OEE, power flows. They install with the shadcn CLI into a React 19 and Tailwind CSS 4 project, in light and dark. They are interface components only: they do not connect to your PLCs, and they replace neither a control system nor your safety analysis. If your stack is Angular or plain HTML, the hmi-kit web components cover process graphics, trends, alarms and confirmed commands in the same style. For the underlying principles, see High-performance HMI: 8 rules for a web screen.

Sources

More guides