Alert card
The alert as the crew meets it — its priority, what happened, and what to do about it.
On this page
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { Reading } from "@bosunkit/components/reading";
import { AnchorIec } from "@bosunkit/icons/anchor/AnchorIec";
import { ReadoutSize } from "@bosunkit/instruments";
import { Phone } from "lucide-react";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"An anchor drag nobody has acknowledged, its anchor flashing lightly on the alarm clock, over the same alarm " +
"once Maya Lindqvist has acknowledged it, then a warning, a caution, a board message and a cleared alarm.";
/** Seconds the unacknowledged alarm has run when the example opens. */
const RAISED_SECONDS: number = 12;
/** Seconds the acknowledged alarm has run when the example opens. */
const ACKNOWLEDGED_ALARM_SECONDS: number = 160;
/** How long the example's acknowledgement takes to come back from the board. */
const BOARD_CONFIRMS_AFTER_MS: number = 600;
/** How long the example shows the acknowledged card before it raises the alarm again. */
const RAISE_AGAIN_AFTER_SECONDS: number = 8;
function minutesAndSeconds(total: number): string {
return `${String(Math.floor(total / 60))}:${String(total % 60).padStart(2, "0")}`;
}
/** The UTC clock time this many seconds after the alarm was raised at 02:14. */
function clockAfterRaised(seconds: number): string {
const minutes: number = 2 * 60 + 14 + Math.floor(seconds / 60);
return `${String(Math.floor(minutes / 60)).padStart(2, "0")}:${String(minutes % 60).padStart(2, "0")}`;
}
function ignore(): void {
// The example calls nobody.
}
/** The example's clock: seconds since it opened, and the second the board confirmed the acknowledgement. */
interface Watch {
readonly ticks: number;
readonly acknowledgedAt: number | null;
}
/** One second on: the acknowledged alarm is raised again once it has shown for a while. */
function tick(watch: Watch): Watch {
const ticks: number = watch.ticks + 1;
const raisedAgain: boolean =
watch.acknowledgedAt !== null && ticks - watch.acknowledgedAt >= RAISE_AGAIN_AFTER_SECONDS;
return { ticks, acknowledgedAt: raisedAgain ? null : watch.acknowledgedAt };
}
export default function Showcase(): ReactElement {
const [watch, setWatch]: [Watch, Dispatch<SetStateAction<Watch>>] = useState<Watch>({
ticks: 0,
acknowledgedAt: null,
});
const { ticks, acknowledgedAt }: Watch = watch;
useEffect((): (() => void) => {
const timer: ReturnType<typeof setInterval> = setInterval((): void => {
setWatch(tick);
}, 1000);
return (): void => {
clearInterval(timer);
};
}, []);
function acknowledge(): Promise<void> {
return new Promise<void>((resolve: () => void): void => {
setTimeout((): void => {
setWatch((current: Watch): Watch => ({ ...current, acknowledgedAt: current.ticks }));
resolve();
}, BOARD_CONFIRMS_AFTER_MS);
});
}
const deliveryChannels: { channel: string; status: "sent" | "read" | "failed" | "pending" }[] = [
{ channel: "voice", status: "sent" },
{ channel: "telegram", status: "read" },
{ channel: "whatsapp", status: "failed" },
{ channel: "push", status: "pending" },
];
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-lg)", width: "100%" }}>
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)" }}>
<AlertCard
priority="alarm"
title="Anchor drag"
causeIcon={<AnchorIec />}
time="02:14"
timeReference="UTC"
elapsed={minutesAndSeconds(RAISED_SECONDS + ticks)}
message={
"SV Althea is dragging out of the swing circle in Cala Portals. The wind has backed southwest and is " +
"gusting 28 kn onto the moorings to the north."
}
immediate
acknowledged={acknowledgedAt !== null}
acknowledgedBy="Maya Lindqvist"
acknowledgedAt={acknowledgedAt === null ? undefined : clockAfterRaised(RAISED_SECONDS + acknowledgedAt)}
acknowledgeAction={acknowledge}
badges={
<>
<Badge variant="red" unacknowledged={acknowledgedAt === null}>
Anchor watch
</Badge>
<Badge variant="dim">Maya Lindqvist on watch</Badge>
</>
}
deliveryChannels={deliveryChannels}
advisory={[
"Start the engine and motor gently toward the anchor to take the strain off the rode.",
"Check the bearing to the anchor, 212°, against the chart: the moorings lie 60 m to the north.",
"If she keeps dragging, weigh anchor and re-anchor in 8 m of sand south of the swimming buoys.",
"Log the time, the position and the scope let out once she is holding.",
]}
actions={
<Button variant="ghost" onClick={ignore}>
<Phone size={14} aria-hidden="true" />
Call the skipper
</Button>
}
figures={[
{ label: "Outside circle", value: "49 m" },
{ label: "Bearing to anchor", value: "212°" },
{ label: "Chain out", value: "40 m" },
]}
>
<div style={{ display: "flex", gap: "var(--space-xl)", flexWrap: "wrap" }}>
<Reading value={8.4} unit="m" label="Depth" source="DST810" fractionDigits={1} size={ReadoutSize.small} />
<Reading value={28} unit="kn" label="Gust" source="WS320" trend="rising" size={ReadoutSize.small} />
<Reading value={231} unit="°T" label="TWD" source="WS320" size={ReadoutSize.small} />
</div>
</AlertCard>
<AlertCard
priority="alarm"
title="Anchor drag"
causeIcon={<AnchorIec />}
time="01:52"
timeReference="UTC"
elapsed={minutesAndSeconds(ACKNOWLEDGED_ALARM_SECONDS + ticks)}
message="Kaia is dragging past the moored catamaran astern in Cala Portals; the crew are weighing anchor."
acknowledged
acknowledgedBy="Maya Lindqvist"
acknowledgedAt="01:53"
deliveryChannels={[
{ channel: "voice", status: "sent" },
{ channel: "telegram", status: "read" },
]}
actions={
<Button onClick={ignore}>
<Phone size={14} aria-hidden="true" />
Call the skipper
</Button>
}
figures={[
{ label: "Outside circle", value: "31 m" },
{ label: "Chain out", value: "35 m" },
]}
/>
</div>
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(280px, 1fr))",
gap: "var(--space-md)",
}}
>
<AlertCard
priority="warning"
title="Wind above reef threshold"
time="01:58"
timeReference="UTC"
message="Gusts of 24 kn for the last ten minutes; the reef is set for 20 kn."
figures={[
{ label: "Gust", value: "24 kn" },
{ label: "Reef at", value: "20 kn" },
]}
/>
<AlertCard
priority="caution"
title="House bank low"
time="01:20"
timeReference="UTC"
message="State of charge 38 % after the anchor light, fridge and autopilot ran overnight."
acknowledged
acknowledgedBy="Tom Hale"
acknowledgedAt="01:24"
badges={<Badge variant="amber">Charge before dawn</Badge>}
/>
<AlertCard
priority="none"
title="Tide turns at 03:40"
time="00:30"
timeReference="UTC"
message="High water at Palma, 0.4 m; the swing circle shifts to the northeast with the stream."
/>
<AlertCard
priority="alarm"
title="Bilge pump running"
time="23:47"
timeReference="UTC"
message="The forward bilge pump ran for 90 s and stopped; the bilge is dry."
cleared
/>
</div>
</div>
);
}
The alert card is the alert as the crew meets it: one card carrying the priority, what happened, when it happened and what to do about it. The priority system behind it — which conditions become which priority, and what each one demands — belongs to Alarms and severity.
Examples
Alarm above an acknowledged caution
Unacknowledged alarm
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { AnchorIec } from "@bosunkit/icons/anchor/AnchorIec";
import type { ReactElement } from "react";
export const description: string =
"An alarm nobody has acknowledged: the heaviest edge, the alarm shape beside the color, how long it has run, and " +
"its anchor flashing lightly behind the text on the alarm clock.";
function acknowledge(): Promise<void> {
// The example has no board to tell; the card stays unacknowledged.
return Promise.resolve();
}
export default function UnacknowledgedAlarm(): ReactElement {
return (
<AlertCard
priority="alarm"
title="Anchor drag"
causeIcon={<AnchorIec />}
message="38 m outside the swing circle"
time="21:02"
timeReference="UTC"
elapsed="0:12"
acknowledgeAction={acknowledge}
/>
);
}
An alarm is the priority that needs hands on the boat: it draws the heaviest edge, and while nobody has acknowledged
it its frame flashes a ring on the shared alarm clock. The icon for what the alert is about (causeIcon, an
OpenBridge icon such as the anchor) sits large and faint behind the text and swells and fades lightly, brightest
while the frame is lit, on the same clock as every other alarm flash on the screen; both keep going under reduced
motion and stop on acknowledge. Only an unacknowledged alarm moves; a warning's or a caution's mark does not. The
card's own button acknowledges the alert it names and shows pending until the board confirms. If the acknowledgement
fails, the card says so under its buttons, the alert stays unacknowledged, and the button is offered again. Pending
lasts past bosund's answer until the board shows the alert acknowledged, so a second press sends nothing; if the board
has not shown it 10 seconds after bosund answered, the card says so and offers the button again.
Warning
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import type { ReactElement } from "react";
export const description: string = "A warning, which needs attention in due course and does not flash.";
export default function Warning(): ReactElement {
return <AlertCard priority="warning" title="Anchor drag" time="21:02" timeReference="UTC" />;
}
A warning needs attention in due course.
Caution
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import type { ReactElement } from "react";
export const description: string = "A caution, the early word that a condition is developing.";
export default function Caution(): ReactElement {
return <AlertCard priority="caution" title="Anchor drag" time="21:02" timeReference="UTC" />;
}
A caution is the early word that a condition is developing.
Information
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import type { ReactElement } from "react";
export const description: string = "A board message with no priority, drawn in the neutral color the messages use.";
export default function Information(): ReactElement {
return <AlertCard priority="none" title="Anchor drag" time="21:02" timeReference="UTC" />;
}
Information carries a board message with no priority at all, in the neutral color the messages use. Its priority
is none.
With advisory and figures
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Button } from "@bosunkit/components/button";
import { Phone } from "lucide-react";
import type { ReactElement } from "react";
export const description: string =
"An immediate alarm with its message, how long it has run, the channels it has reached, a response advisory " +
"folded behind its own control, and its actions on one row with the figures behind the decision.";
function acknowledge(): Promise<void> {
// The example has no board to tell; the card stays unacknowledged.
return Promise.resolve();
}
function ignore(): void {
// The example calls nobody.
}
export default function WithAdvisoryAndStats(): ReactElement {
return (
<AlertCard
priority="alarm"
title="Anchor drag"
time="21:02"
timeReference="UTC"
elapsed="3:04"
message="The vessel has moved 42 m from the anchor position."
immediate
figures={[
{ label: "From anchor", value: "42 m" },
{ label: "Bearing to anchor", value: "212°" },
]}
advisory={["Check the anchor rode.", "Start the engine."]}
acknowledgeAction={acknowledge}
actions={
<Button variant="ghost" onClick={ignore}>
<Phone size={14} aria-hidden="true" />
Call the skipper
</Button>
}
deliveryChannels={[
{ channel: "voice", status: "sent" },
{ channel: "telegram", status: "read" },
]}
/>
);
}
The figures are the values behind the decision, each with its label, at the right of the actions' row, so the figure and the button that answers it are read together; on a narrow card they wrap under the actions. How long the alert has run sits large at the top right of the title row, over the time it was raised and the time's reference. The delivery channels say how far the alert has traveled and how each handover went. The response advisory stays folded behind its own control until it is wanted, because a card that is already shouting does not need to be taller than the screen it sits on.
Acknowledged
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { AnchorIec } from "@bosunkit/icons/anchor/AnchorIec";
import type { ReactElement } from "react";
export const description: string =
"An alarm the crew has acknowledged: who acknowledged it and when on one line above its actions, the anchor " +
"steady and the frame calm, and the red edge kept until the condition clears.";
export default function Acknowledged(): ReactElement {
return (
<AlertCard
priority="alarm"
title="Anchor drag"
causeIcon={<AnchorIec />}
message="38 m outside the swing circle"
time="21:02"
timeReference="UTC"
elapsed="1:40"
acknowledged
acknowledgedBy="Maya Lindqvist"
acknowledgedAt="21:03"
figures={[{ label: "Outside circle", value: "38 m" }]}
/>
);
}
Once the board reports the alert acknowledged, the acknowledge button goes and the card says who acknowledged it and when on one line above its actions, its cause mark stops flashing and its frame is steady. An acknowledged alarm keeps its red edge until the condition clears.
Acknowledged and cleared
Acknowledging states that the crew has seen the alert without saying the condition has ended. A cleared card dims to about a third, stops responding to the pointer and carries a cleared tag, which is how the board says the condition is over rather than that someone hid the card.
On a narrow screen
On a phone or a tablet, a screen up to the 68.75em breakpoint (1,100 px at 16 px), the response advisory's toggle
grows to the 48 px touch target. The width is the screen's the card sits in, not the window's: a page, an app's main area or a phone frame declares itself the screen
with container: bk-screen / inline-size, and outside one the toggle keeps its compact height. A touch screen takes
the touch target at any width.
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import type { ReactElement } from "react";
export const description: string =
"A 360 px screen: the response advisory's toggle grows to the 48 px touch target, as on a phone.";
export default function OnANarrowScreen(): ReactElement {
return (
<div style={{ container: "bk-screen / inline-size", width: "100%", maxWidth: 360 }}>
<AlertCard
priority="alarm"
title="Anchor drag"
time="03:12"
timeReference="UTC"
message="Althea is 41 m from where the anchor was dropped, outside the 35 m swing radius."
advisory={["Start the engine and take the strain off the rode.", "Wake the skipper."]}
/>
</div>
);
}
Motion
Inside an alert list (card grid) a card moves by default: it rises in when the alert
arrives, fades out with the fast exit when it clears, and slides to its new place when the order changes. Outside a
list it moves when the caller passes motion, and motion={false} turns it off anywhere. The title and the message
are in the page and announced from the first frame; the entrance only fades and slides over them, for at most the
medium motion duration. A leaving card takes no press. A change of priority and an
acknowledgment change the card at once, and only an unacknowledged alarm flashes.
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Button } from "@bosunkit/components/button";
import { MotionPresence } from "@bosunkit/components/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"An alarm that rises in when it is raised, its text readable from the first frame, and fades out when it clears.";
export default function Motion(): ReactElement {
const [raised, setRaised]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "min(100%, 460px)" }}>
<Button
size="sm"
onClick={(): void => {
setRaised((current: boolean): boolean => !current);
}}
>
{raised ? "The anchor holds again" : "The anchor drags"}
</Button>
<div style={{ minHeight: 96 }}>
<MotionPresence>
{raised && (
<AlertCard
key="drag"
priority="alarm"
title="Anchor drag"
message="38 m outside the swing circle"
time="21:02"
timeReference="UTC"
motion
/>
)}
</MotionPresence>
</div>
</div>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| priorityrequired | Priority | – | The alert's priority; it sets the card's color, shape and whether an unacknowledged card flashes. |
| titlerequired | string | – | What the alert is about. |
| message | ReactNode | undefined | – | The alert's explanation, under the title. |
| time | string | undefined | – | When the alert was raised, as the caller displays it (02:14); printed with timeReference. |
| timeReference | string | undefined | – | The reference the card's times are given in (UTC), printed after each of them. |
| elapsed | string | undefined | – | How long the alert has been active, formatted by the caller (0:12): drawn large at the top right of the title
row, over the time it was raised. |
| acknowledged | boolean | undefined | false | Whether the crew have acknowledged the alert, as bosund's board reports it. An acknowledged card drops its acknowledge button, says who acknowledged it and when on a line above its actions, and stops flashing; an alarm keeps its red edge until it clears. |
| acknowledgedBy | string | undefined | – | Who acknowledged the alert, as displayed (the board's acknowledged_by). |
| acknowledgedAt | string | undefined | – | When the alert was acknowledged, as the caller displays it (02:15); printed with timeReference. |
| acknowledgeAction | (() => Promise<void>) | undefined | – | Acknowledges this alert. While the alert is unacknowledged and its priority needs acknowledging, the card shows an
acknowledge button named for the alert, pending until acknowledged says the board shows it (or, after the action
resolves, for at most ACKNOWLEDGE_CONFIRM_MS); the card shows the alert as acknowledged only once acknowledged
says so, and says why when a press does not acknowledge it. |
| cleared | boolean | undefined | false | Whether the alert has cleared. |
| causeIcon | ReactNode | undefined | – | An icon for what the alert is about, from @bosunkit/icons (<AnchorIec /> for an anchor drag), drawn large and
faint behind the card's content and hidden from assistive technology. While an alarm is unacknowledged it flashes
lightly with the card's frame, on the shared alarm clock; otherwise it stays still. |
| immediate | boolean | undefined | false | Whether the alert needs the crew now; the card then draws its immediate edge. |
| badges | ReactNode | undefined | – | Badges shown with the delivery channels. |
| figures | readonly AlertFigure[] | undefined | – | Figures at the right of the actions' row, red in an alarm; they wrap under the actions on a narrow card. |
| advisory | readonly string[] | undefined | – | Response steps, behind a "Response advisory" toggle. |
| actions | ReactNode | undefined | – | Buttons for the alert, such as acknowledge. |
| deliveryChannels | readonly DeliveryChannel[] | undefined | – | The channels the alert went out on and how far each got. |
| children | ReactNode | undefined | – | Content between the status line and the advisory. |
| className | string | undefined | – | Classes added to the card's own. |
| testId | string | undefined | – | Becomes the card's data-testid; the cause mark is ${testId}--mark. |
| motion | MotionProp | undefined | – | How the card moves. Inside a moving alert list (AlertCardGrid) it rises in when the alert arrives, fades out when
it clears and slides to its new place when the order changes; elsewhere it moves only when asked. false turns it
off, a preset (fade, rise, scale) or { enter, exit } chooses. The alert's text is in the page and
announced from the first frame; the entrance only fades and slides over it. |
Related
- Alarms and severity — the priority system, its shapes and its words.
- Button and Badge — the actions and labels a card carries.
- Accessibility — the contrast floor, the target sizes and the keyboard path.
- Data integrity — the reading states that raise some of these alerts.