Skip to content
design
Components

Alert list row

One alert in an alert list, with its priority's icon and word, its state, its time and its acknowledge button.

On this page
Press a row to open its alert.
The alert list at anchor at night: an alarm waiting for acknowledgement, an acknowledged one, a silenced warning, a cleared one and an active caution.
Code
import { AlertListRow, type AlertListRowState } from "@bosunkit/components/alert-list-row";
import type { Priority } from "@bosunkit/components/priority";
import { useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The alert list at anchor in Cala Portals at 02:20 UTC, by priority and then by time: the anchor alarm waits for " +
  "acknowledgement and shows pending until the acknowledgement is confirmed, a close-quarters alarm is acknowledged, " +
  "a low battery warning is silenced, a bilge warning has cleared without acknowledgement, and a wind sensor " +
  "caution is active. Press a row to open its alert.";

interface Alert {
  readonly id: string;
  readonly priority: Exclude<Priority, "none">;
  readonly title: string;
  readonly description: string;
  readonly source: string;
  readonly time: string;
  readonly state: AlertListRowState;
}

const RANK: Readonly<Record<Exclude<Priority, "none">, number>> = { emergency: 0, alarm: 1, warning: 2, caution: 3 };

const ALERTS: readonly Alert[] = [
  {
    id: "anchor",
    priority: "alarm",
    title: "Anchor dragging",
    description: "58 m from the drop point, outside the 45 m swing circle. Start the engine and re-anchor.",
    source: "Anchor watch",
    time: "02:16",
    state: "unacknowledged",
  },
  {
    id: "cpa",
    priority: "alarm",
    title: "Close quarters, AR MEN",
    description: "The fishing vessel passes 0.2 NM off in 6 minutes on her present course.",
    source: "AIS",
    time: "02:09",
    state: "acknowledged",
  },
  {
    id: "battery",
    priority: "warning",
    title: "House bank at 11.9 V",
    description: "The anchor light and the fridge are drawing 6 A. Start the engine or switch the fridge off.",
    source: "Battery monitor",
    time: "02:12",
    state: "silenced",
  },
  {
    id: "bilge",
    priority: "warning",
    title: "Bilge pump ran 6 times in an hour",
    description: "The bilge is dry now. Check the stern gland when you go below.",
    source: "Bilge",
    time: "01:30",
    state: "cleared",
  },
  {
    id: "wind",
    priority: "caution",
    title: "Masthead wind sensor silent",
    description: "No reading for 4 minutes. Wind speed and direction show a dash until it reports again.",
    source: "Instruments",
    time: "02:02",
    state: "unacknowledged",
  },
];

/** How long the example takes to confirm an acknowledgement, as bosund's next board update would. */
const CONFIRM_MILLISECONDS: number = 1200;

const LIST: CSSProperties = {
  width: "100%",
  maxWidth: 680,
  margin: 0,
  padding: 0,
  border: "1px solid var(--border)",
  borderRadius: "var(--radius-md)",
  overflow: "hidden",
};

function ordered(alerts: readonly Alert[]): Alert[] {
  return [...alerts].sort((left: Alert, right: Alert): number => {
    const byPriority: number = RANK[left.priority] - RANK[right.priority];
    return byPriority !== 0 ? byPriority : right.time.localeCompare(left.time);
  });
}

export default function Showcase(): ReactElement {
  const [alerts, setAlerts]: [readonly Alert[], Dispatch<SetStateAction<readonly Alert[]>>] =
    useState<readonly Alert[]>(ALERTS);
  const [opened, setOpened]: [string, Dispatch<SetStateAction<string>>] = useState<string>("");

  function acknowledge(id: string): () => Promise<void> {
    return (): Promise<void> =>
      new Promise<void>((resolve: () => void): void => {
        window.setTimeout((): void => {
          // A cleared alert leaves the list once it is acknowledged (MSC.302); any other reads acknowledged.
          setAlerts((current: readonly Alert[]): readonly Alert[] =>
            current.flatMap((alert: Alert): Alert[] => {
              if (alert.id !== id) {
                return [alert];
              }
              return alert.state === "cleared" ? [] : [{ ...alert, state: "acknowledged" }];
            }),
          );
          resolve();
        }, CONFIRM_MILLISECONDS);
      });
  }

  return (
    <div style={{ width: "100%", display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
      <ul style={LIST}>
        {ordered(alerts).map((alert: Alert): ReactElement => (
          <AlertListRow
            key={alert.id}
            priority={alert.priority}
            title={alert.title}
            description={alert.description}
            source={alert.source}
            time={alert.time}
            timeReference="UTC"
            state={alert.state}
            acknowledgeAction={acknowledge(alert.id)}
            onPress={(): void => {
              setOpened(alert.title);
            }}
          />
        ))}
      </ul>
      <span aria-live="polite" style={{ color: "var(--text-dim)" }}>
        {opened === "" ? "Press a row to open its alert." : `Opened: ${opened}`}
      </span>
    </div>
  );
}

An alert list row shows one alert as OpenBridge's alert list draws it: the alert icon for its priority, the title and a line saying what is wrong and what to do, the priority, the system that raised it and the state in words, and the time with its reference. Rows go in a list, grouped by priority and then by time (MSC.302 §9.16).

The icon carries the priority's IEC 62923 shape (▲ alarm, ● warning, ■ caution) and its state. An unacknowledged alarm or emergency flashes on the shared blink clock, in step with every other alert on screen, and keeps flashing under reduced motion; a warning is steady, as a caution is; nothing else in the row moves. The priority's color runs down the row's leading edge, and its word sits beside the source, so the row never says its priority by color alone.

01Priority firstThe shape, the word and the color of the priority come from the shared mapping, so every alert reads the same on every screen.
02One alert at a timeThe acknowledge button names the alert it acknowledges, and there is no button that acknowledges them all.
03Never ahead of the boatAn acknowledgement shows pending until the row reads acknowledged, which it does only when the board says so; after 10 seconds without that, the row says the board has not shown it and offers the button again.

States

StateWhenIconAcknowledge button
unacknowledgedRaised, and nobody has acknowledged it.FlashesOffered
silencedIts sound is off for now, and it needs acknowledging.Flashes, silencedOffered
acknowledgedAcknowledged while its cause is present.SteadyNone
clearedIts cause has cleared before anyone acknowledged it.Flashes, rectifiedOffered

A caution needs no acknowledging: it reads Active while it lasts and Cleared after, and offers no button. An acknowledged alert whose cause clears leaves the list.

Examples

Priorities

  • Person overboardJoost's lifejacket beacon triggered 0.1 NM astern.Emergency alarmAIS beaconUnacknowledged02:14 UTC
  • Depth below 3.0 m2.6 m under the keel and shoaling.AlarmDepthUnacknowledged02:11 UTC
  • Engine coolant 92 °C and risingReduce to 1800 rpm and check the raw-water strainer.WarningEngineUnacknowledged02:09 UTC
  • Fuel tank at 18 percentAbout 40 L left, enough for 7 hours at cruising revs.CautionTanksActive01:48 UTC
The four priorities, unacknowledged: an emergency alarm and an alarm flash with the alarm icon, a warning flashes more slowly with its own, and a caution is steady. Each says its priority in words.
Code
import { AlertListRow } from "@bosunkit/components/alert-list-row";
import type { ReactElement } from "react";

export const description: string =
  "The four priorities, unacknowledged: an emergency alarm and an alarm flash with the alarm icon, a warning flashes " +
  "more slowly with its own, and a caution is steady. Each says its priority in words.";

export default function Priorities(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AlertListRow
        priority="emergency"
        title="Person overboard"
        description="Joost's lifejacket beacon triggered 0.1 NM astern."
        source="AIS beacon"
        time="02:14"
        timeReference="UTC"
        state="unacknowledged"
      />
      <AlertListRow
        priority="alarm"
        title="Depth below 3.0 m"
        description="2.6 m under the keel and shoaling."
        source="Depth"
        time="02:11"
        timeReference="UTC"
        state="unacknowledged"
      />
      <AlertListRow
        priority="warning"
        title="Engine coolant 92 °C and rising"
        description="Reduce to 1800 rpm and check the raw-water strainer."
        source="Engine"
        time="02:09"
        timeReference="UTC"
        state="unacknowledged"
      />
      <AlertListRow
        priority="caution"
        title="Fuel tank at 18 percent"
        description="About 40 L left, enough for 7 hours at cruising revs."
        source="Tanks"
        time="01:48"
        timeReference="UTC"
        state="unacknowledged"
      />
    </ul>
  );
}

States

  • Anchor draggingAlarmAnchor watchUnacknowledged02:16 UTC
  • Anchor draggingAlarmAnchor watchSilenced02:16 UTC
  • Anchor draggingAlarmAnchor watchAcknowledged02:16 UTC
  • Anchor draggingAlarmAnchor watchCleared, unacknowledged02:16 UTC
One alarm in each state: unacknowledged and silenced flash, acknowledged is steady, and cleared keeps flashing until someone acknowledges it.
Code
import { AlertListRow, type AlertListRowState } from "@bosunkit/components/alert-list-row";
import type { ReactElement } from "react";

export const description: string =
  "One alarm in each state: unacknowledged and silenced flash, acknowledged is steady, and cleared keeps flashing " +
  "until someone acknowledges it.";

const STATES: readonly AlertListRowState[] = ["unacknowledged", "silenced", "acknowledged", "cleared"];

export default function States(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      {STATES.map((state: AlertListRowState): ReactElement => (
        <AlertListRow
          key={state}
          priority="alarm"
          title="Anchor dragging"
          source="Anchor watch"
          time="02:16"
          timeReference="UTC"
          state={state}
        />
      ))}
    </ul>
  );
}

Acknowledging

  • House bank at 11.9 VStart the engine or switch the fridge off.WarningBattery monitorUnacknowledged02:12 UTC
Acknowledging one alert: the button reads Acknowledging… until the action settles, and the row reads Acknowledged only when its state says so, here 1.5 s later as bosund's next board update would.
Code
import { AlertListRow, type AlertListRowState } from "@bosunkit/components/alert-list-row";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Acknowledging one alert: the button reads Acknowledging… until the action settles, and the row reads " +
  "Acknowledged only when its state says so, here 1.5 s later as bosund's next board update would.";

export default function Acknowledge(): ReactElement {
  const [state, setState]: [AlertListRowState, Dispatch<SetStateAction<AlertListRowState>>] =
    useState<AlertListRowState>("unacknowledged");

  function acknowledge(): Promise<void> {
    return new Promise<void>((resolve: () => void): void => {
      window.setTimeout((): void => {
        setState("acknowledged");
        resolve();
      }, 1500);
    });
  }

  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AlertListRow
        priority="warning"
        title="House bank at 11.9 V"
        description="Start the engine or switch the fridge off."
        source="Battery monitor"
        time="02:12"
        timeReference="UTC"
        state={state}
        acknowledgeAction={acknowledge}
      />
    </ul>
  );
}

A failed acknowledgement

  • Bilge water highThe float switch in the main bilge is up.AlarmBilgeUnacknowledged02:18 UTC
An acknowledgement that fails: the alarm stays unacknowledged and flashing, the row says so, and the button offers it again.
Code
import { AlertListRow } from "@bosunkit/components/alert-list-row";
import type { ReactElement } from "react";

export const description: string =
  "An acknowledgement that fails: the alarm stays unacknowledged and flashing, the row says so, and the button " +
  "offers it again.";

function unreachable(): Promise<void> {
  return new Promise<void>((_resolve: () => void, reject: (reason: Error) => void): void => {
    window.setTimeout((): void => {
      reject(new Error("bosund did not answer"));
    }, 800);
  });
}

export default function Failed(): ReactElement {
  return (
    <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
      <AlertListRow
        priority="alarm"
        title="Bilge water high"
        description="The float switch in the main bilge is up."
        source="Bilge"
        time="02:18"
        timeReference="UTC"
        state="unacknowledged"
        acknowledgeAction={unreachable}
      />
    </ul>
  );
}

Opening an alert

Not opened yet.
With `onPress` the alert is a button named by its title, which Enter and Space press, and the acknowledge button is the next tab stop.
Code
import { AlertListRow } from "@bosunkit/components/alert-list-row";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "With `onPress` the alert is a button named by its title, which Enter and Space press, and the acknowledge button " +
  "is the next tab stop.";

export default function Press(): ReactElement {
  const [presses, setPresses]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  return (
    <div style={{ width: "100%", display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
      <ul style={{ width: "100%", maxWidth: 640, margin: 0, padding: 0 }}>
        <AlertListRow
          priority="caution"
          title="Masthead wind sensor silent"
          description="No reading for 4 minutes."
          source="Instruments"
          time="02:02"
          timeReference="UTC"
          state="unacknowledged"
          onPress={(): void => {
            setPresses(presses + 1);
          }}
        />
      </ul>
      <span aria-live="polite" style={{ color: "var(--text-dim)" }}>
        {presses === 0 ? "Not opened yet." : `Opened ${String(presses)} times.`}
      </span>
    </div>
  );
}

Props

PropTypeDefaultDescription
priorityrequiredExclude<Priority, "none">–The alert's priority: its IEC 62923 shape, its color, its word and whether it flashes and needs acknowledging.
titlerequiredstring–What the alert is about, translated by the caller.
descriptionReactNode | undefined–A line under the title: what is wrong and what to do.
sourcestring | undefined–The system or sensor that raised the alert, translated by the caller (Anchor watch, Engine).
timerequiredstring–When the alert was raised, as the caller writes it (02:16).
timeReferencerequiredstring–The reference time is in, printed after it (UTC).
staterequiredAlertListRowState–Where the alert is in its lifecycle.
acknowledgeAction(() => Promise<void>) | undefined–Acknowledges this alert. The row offers its acknowledge button while the alert needs acknowledging and this is set, shows it pending until state says acknowledged (or, after the action resolves, for at most ACKNOWLEDGE_CONFIRM_MS), shows the alert acknowledged only once state says so, and says why when a press does not acknowledge it.
onPress(() => void) | undefined–Called when the crew press the row, to open the alert's detail; the row is then a button.
classNamestring | undefined–Classes added to the row's own.
testIdstring | undefined–data-testid on the row; its acknowledge button is ${testId}--ack.
refRef<HTMLLIElement> | undefined–The row's <li> element.
Do
  • Anchor draggingAlarmAnchor watchUnacknowledged02:16 UTC
  • House bank at 11.9 VWarningBattery monitorSilenced02:18 UTC
Group the list by priority, then by time: the alarm sits above the later warning.
Don't
  • House bank at 11.9 VWarningBattery monitorSilenced02:18 UTC
  • Anchor draggingAlarmAnchor watchUnacknowledged02:16 UTC
Don't order the list by time alone. A later warning pushes the alarm down, out of the crew's first look.
  • Alert icon: the icon each row draws, in every state.
  • Alert card: one alert with its advisory and delivery, for the alert's own view.
  • Alarms and severity: the priorities, their shapes and the flash rates.