# Building an industrial monitoring dashboard with React and shadcn/ui

Source : https://consignestudio.fr/en/guides/react-shadcn-industrial-dashboard/ · published 2026-10-27 · Antoine Despres, Consigne

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.

## 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:

| Token | Use | Never use it for |
|---|---|---|
| `alarm-1` to `alarm-4` | alarm priorities, 1 critical to 4 low | decoration, "success" states |
| `quality` | uncertain, bad or frozen data | process alarms |
| `process` | running equipment, nominal values, as a dark neutral | anything that must catch the eye |
| `track`, `track-normal` | indicator 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:

```ts
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:

```ts
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.

## Trends: fixed scale, no animation

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:

| Active | Acknowledged | Meaning | Display |
|---|---|---|---|
| yes | no | new, nobody has seen it | priority colour, slow blink |
| yes | yes | seen, the cause is still there | priority colour, steady |
| no | no | cleared before anyone saw it | stays in the list until acknowledged |
| no | yes | finished | leaves 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.

> Turn blinking off when the user prefers reduced motion (`prefers-reduced-motion: reduce`). The priority must remain readable without it, through its colour, number and label.

## 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](https://consignestudio.fr/en/shadcn-industrial-dashboard/) 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](https://consignestudio.fr/en/hmi-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](https://consignestudio.fr/en/guides/high-performance-hmi-rules/).

## Sources

- ANSI/ISA-101.01-2015, Human Machine Interfaces for Process Automation Systems
- ANSI/ISA-18.2 and IEC 62682, management of alarm systems (not reproduced)
- [Control Engineering, Implementing alarm management per the ANSI/ISA-18.2 standard (alarm rate benchmarks)](https://www.yokogawa.com/be-fr/library/resources/media-publications/implementing-alarm-management-per-the-ansi-isa-182-standard-control-engineering/)
- OPC Foundation, OPC UA status codes (Good, Uncertain, Bad)
