Source Code

<div class="demo">
  <div class="tile">
    <div class="tile-head">
      <h3>Active incidents</h3>
      <span class="count-badge" id="countBadge">2</span>
    </div>

    <div class="incidents" id="incidents"></div>

    <div class="tile-foot">
      <div class="foot-stat">
        <span class="foot-label">Avg time to resolve</span>
        <span class="foot-val" id="mttr">38m</span>
      </div>
      <div class="foot-stat">
        <span class="foot-label">Resolved today</span>
        <span class="foot-val" id="resolvedToday">5</span>
      </div>
    </div>
  </div>
</div>

Incident Status Summary Widget — Free HTML CSS JS Snippet

Incident Status Summary Widget · Dashboards · Plain HTML, CSS & JS · Live preview

What's included

Features

Severity communicated redundantly through pill text, border color, and background tint for fast scanning
Per-incident live duration counter updates every second without re-rendering the full list
Resolve transition fades and slides a row out before removing it from the array, avoiding an abrupt jump
MTTR recomputed as a true running average on each resolution, not a static or manually-set number
Automatic calm empty state once the active incidents array is empty
Count badge switches color between "incidents active" red and "all clear" green states
Structured to plug into a real incident-management API (PagerDuty/Opsgenie-style) with minimal changes
Compact tile layout suited to sitting in a grid of other ops dashboard widgets

About this UI Snippet

Incident Status Summary Widget — Severity-Ranked List, Live Duration & Rolling MTTR

Screenshot of the Incident Status Summary Widget snippet rendered live

An internal ops dashboard usually has one question that matters more than any other: is anything on fire right now, and how bad? This widget answers it with a severity-ranked list of active incidents, each showing a live elapsed-time counter, plus two rolling statistics — mean time to resolve (MTTR) and incidents resolved today — that update in place as incidents get resolved.

Severity as both a pill and a border color, not just text

Each incident row gets a sev1/sev2/sev3 class that drives both its left border accent color and its background tint, in addition to the SEV1/SEV2/SEV3 pill. Redundant severity encoding (color plus text plus position, since the array is not otherwise sorted) matters here because an incident list is exactly the kind of interface someone scans in a hurry — a single color-blind-unfriendly signal, or text alone requiring careful reading, is worse for a screen someone glances at mid-triage than layered, redundant cues.

A duration that ticks without re-rendering the whole list

tickDurations() runs every second and updates only each row's .inc-duration text node directly via querySelector, rather than calling the full render() function that rebuilds the entire incidents list from scratch. This distinction matters at scale: rebuilding innerHTML every second on a dashboard tile that might sit open for hours is needless DOM churn, and worse, it would interrupt anything mid-interaction (like a hover state) purely to update a timestamp text node.

Resolving is a two-step transition, not an instant removal

resolveIncident() first adds a .resolving class that CSS fades and slides the row out over 300ms, and only *after* that setTimeout delay does it actually splice the incident out of the array and call render(). An incident disappearing from a monitoring dashboard the instant it's marked resolved reads as the list glitching; a brief transition confirms to a watching engineer that the removal was intentional, not an error.

MTTR computed as a running average, not a static number

mttrMinutes is not hardcoded after the initial value — resolveIncident() recomputes it as (mttrMinutes * (resolvedTodayCount - 1) + elapsedMin) / resolvedTodayCount, a running average update that folds the just-resolved incident's actual duration into the existing average without needing to store every individual incident's resolve time. This is the same incremental-average technique used anywhere a rolling statistic needs updating on each new data point without recomputing from a full history array every time.

Build with AI

Build, Understand, Optimize, and Extend It With AI

Paste this snippet's HTML, CSS, and JS into an AI coding assistant like Claude and ask it to explain exactly why tickDurations updates individual text nodes directly instead of calling the full render function every second, and what would visibly break or degrade if it called render() instead on a dashboard tile left open for hours. The same assistant can help optimize it — for instance asking whether the running-average MTTR calculation should be weighted differently for very old incidents versus recent ones. It's also useful for extending the widget: ask it to add a "SEV1 only" filter toggle, sort incidents by severity then by duration, or add a subtle audio or visual alert when a new SEV1 incident is added to the array. Treat the code less like a finished artifact and more like a starting point for a conversation.

Prompt to recreate it

Copy this into your AI assistant of choice to build the effect from scratch, or as a jumping-off point for your own variant:

text
Build an "active incidents summary" dashboard widget in HTML, CSS, and vanilla JavaScript — no charting or incident-management library.

Requirements:
- Maintain active incidents as an array of plain objects, each with an id, a numeric severity level, a title, an affected service name, and a start timestamp — render one row per incident showing a severity pill, the title, the service name, and a live elapsed-time duration since it started.
- Encode severity redundantly (not just as text) — each row's border accent color and background tint must also change based on severity level, so severity is readable from color alone as well as from the pill text.
- The per-incident duration must tick upward every second, but the update must only touch each row's existing duration text node directly rather than re-rendering the entire incidents list on every tick.
- Implement a "resolve" function that, given an incident's id, first applies a CSS transition class that fades and slides the row out, and only after that transition's duration elapses removes the incident from the underlying array and re-renders the list — resolving must never remove a row from the DOM instantly with no transition.
- Track a running mean-time-to-resolve statistic that updates incrementally on every resolution using only the previous average and the new incident's duration (do not store or replay a full history array to recompute it), plus a separate resolved-today counter that increments on each resolve.
- When the incidents array becomes empty, replace the list with a single calm "no active incidents" message rather than leaving an empty container, and switch any incident-count badge to a visually distinct "all clear" state.

Want to tighten it up first? Run this prompt through the AI Prompt Studio to score it across 8 quality dimensions, catch anti-patterns, and tune the wording for Claude, ChatGPT, or Gemini before you paste it in.

Step by step

How to Use

  1. 1
    Watch the live duration countersEach active incident's elapsed time updates every second without re-rendering the rest of the list.
  2. 2
    Watch the demo auto-resolveAfter 5 seconds the demo resolves its lowest-severity incident automatically, fading the row out and updating MTTR and the resolved-today count.
  3. 3
    Replace the incidents array with real dataPopulate incidents from your incident-management API (PagerDuty, Opsgenie, or an internal system) with the same { id, sev, title, service, startedAt } shape.
  4. 4
    Wire resolveIncident to a real eventCall resolveIncident(id) whenever your incident tool reports a resolution via webhook or polling, instead of the demo's setTimeout.
  5. 5
    Adjust severity stylingEdit the .sev1/.sev2/.sev3 CSS classes to match your organization's own severity color conventions.
  6. 6
    Show an empty state when nothing is activeThe widget automatically renders a calm "no active incidents" message once the incidents array is empty.

Real-world uses

Common Use Cases

OPS
Internal ops and SRE dashboards
Give an on-call engineer or ops team a live, glanceable view of everything currently on fire without opening a separate incident tool.
ALERT
Status page admin views
Pair with the Uptime Status Page pattern as the internal, severity-detailed counterpart to a public-facing status page.
On-call rotation and escalation tools
Show active incidents alongside on-call schedule context so an engineer sees both who is responsible and what is currently open.
Postmortem and reliability reporting
Feed the rolling MTTR and resolved-count stats into a weekly reliability review, tracking whether response times are trending better or worse.
Related: Scheduled Job Run History Tile
See the Scheduled Job Run History Tile for a related dashboards pattern worth pairing with this one.

Got questions?

Frequently Asked Questions

tickDurations() runs every second but only updates each row's existing .inc-duration text node directly, rather than calling render() and rebuilding the whole incidents list's innerHTML from scratch. This keeps the per-second update cheap even if the incidents list itself is fairly long.

Each row gets a sev1/sev2/sev3 class that simultaneously sets a colored left border, a subtly tinted background, and drives the colored SEV1/SEV2/SEV3 pill text — three redundant visual cues for the same severity level, which matters for a dashboard meant to be scanned quickly under pressure.

resolveIncident() adds a .resolving CSS class that fades and slides the row out over 300ms via a transition, and only removes the incident from the array and re-renders after that delay completes. Removing it instantly would make the list appear to glitch rather than confirming the resolution happened intentionally.

It is a running average, not recomputed from full history each time: mttrMinutes = (mttrMinutes * (resolvedTodayCount - 1) + elapsedMin) / resolvedTodayCount. This folds each newly resolved incident's actual duration into the existing average incrementally.

Replace the static incidents array and the demo's setTimeout auto-resolve with data from your incident tool's API (for example PagerDuty or Opsgenie), keeping the same { id, sev, title, service, startedAt } object shape, and call resolveIncident(id) whenever a webhook or poll reports a real resolution.

render() checks incidents.length and, when zero, replaces the list with a single calm "No active incidents" message and switches the count badge to a green "all clear" style instead of the red active-incident style.