Card
A bordered surface that holds related content under a header, with an optional footer and a priority edge.
On this page
Navigation
Bilge pump
WarningRan 4 times in the last hour, 6 min in all. Check the stern gland and the hose clamps.
Watch notes
Reefed at 12:30 UTC with the wind at 22 kn. Next watch: Marta at 16:00 UTC; keep clear of the Ushant TSS.
Code
import { Button } from "@bosunkit/components/button";
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import { Droplets, Navigation, NotebookPen } from "lucide-react";
import type { CSSProperties, ReactElement } from "react";
export const description: string =
"Three cards from SV Althea's overview at 14:05 UTC: navigation raised with its readings, an action and a source " +
"line; the bilge pump marked as a warning; and the watch notes sunk into the page.";
const READINGS: CSSProperties = { display: "flex", flexWrap: "wrap", gap: "var(--space-lg)" };
export default function Showcase(): ReactElement {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(260px, 1fr))",
gap: "var(--space-md)",
width: "100%",
}}
>
<Card
title="Navigation"
headingLevel={2}
icon={<Navigation />}
surface="raised"
actions={
<Button size="sm" variant="ghost">
Open chart
</Button>
}
footer="GPS and DST810, 14:05 UTC"
>
<div style={READINGS}>
<Reading value={6.8} unit="kn" label="SOG" source="GPS" fractionDigits={1} />
<Reading value={14.2} unit="m" label="Depth" source="DST810" fractionDigits={1} />
<Reading value={218} unit="°T" label="COG" source="GPS" />
</div>
</Card>
<Card
title="Bilge pump"
headingLevel={2}
icon={<Droplets />}
priority="warning"
actions={
<Button size="sm" variant="ghost">
Open log
</Button>
}
footer="Float switch, forward bilge, since 13:12 UTC"
>
<p style={{ margin: 0 }}>
Ran 4 times in the last hour, 6 min in all. Check the stern gland and the hose clamps.
</p>
</Card>
<Card title="Watch notes" headingLevel={2} icon={<NotebookPen />} surface="inset">
<p style={{ margin: 0 }}>
Reefed at 12:30 UTC with the wind at 22 kn. Next watch: Marta at 16:00 UTC; keep clear of the Ushant TSS.
</p>
</Card>
</div>
);
}
A card is a bordered surface that holds content that belongs together: a system's readings, a log, a set of
particulars. Its header carries a title, an icon before it and actions at its end; its footer, under a rule, carries
where the content comes from and when. With a title the card is a region named by it, and the title is a real heading
at headingLevel (3 by default), so the card sits in the page's outline. Without a title it is a plain box. A card is
not an alert: an alert is an alert card, and a list of alerts is a
card grid.
Examples
Titled
Tanks
Code
import { Card } from "@bosunkit/components/card";
import type { ReactElement } from "react";
export const description: string = "A card with a title, which names it as a region.";
export default function Titled(): ReactElement {
return (
<Card title="Tanks" style={{ width: "100%", maxWidth: 360 }}>
Diesel 212 L, water 365 L, holding 18 %.
</Card>
);
}
Header
The icon is decorative and hidden from assistive technology. Actions sit at the end of the header, outside the title, so the card's name stays its title.
VHF
Code
import { Button } from "@bosunkit/components/button";
import { Card } from "@bosunkit/components/card";
import { Radio } from "lucide-react";
import type { ReactElement } from "react";
export const description: string = "An icon before the title and an action at the end of the header.";
export default function Header(): ReactElement {
return (
<Card
title="VHF"
icon={<Radio />}
actions={
<Button size="sm" variant="ghost">
Open radio log
</Button>
}
style={{ width: "100%", maxWidth: 420 }}
>
Channel 16 and 12 by dual watch; last DSC call 09:41 UTC from Brest Port Control.
</Card>
);
}
Footer
The footer sits under a rule in the dim text color. Put the source of the content and the time it was read there, with the time's reference.
Barometer
Code
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string = "A footer under a rule, holding where the value comes from and when.";
export default function Footer(): ReactElement {
return (
<Card title="Barometer" footer="BME280 in the cabin, 14:05 UTC" style={{ width: "100%", maxWidth: 360 }}>
<Reading value={1009} unit="hPa" label="Pressure" source="Cabin" />
</Card>
);
}
Surfaces
surface sets the card on the page (default), above it (raised) or into it (inset). Raise the panel the crew
is working in, and sink notes and secondary detail.
Default
Raised
Inset
Code
import { Card, type CardSurface } from "@bosunkit/components/card";
import type { ReactElement } from "react";
export const description: string = "The default, raised and inset surfaces side by side.";
const SURFACES: readonly { readonly surface: CardSurface; readonly title: string; readonly text: string }[] = [
{ surface: "default", title: "Default", text: "On the page, for most content." },
{ surface: "raised", title: "Raised", text: "Above the page, for the panel the crew is working in." },
{ surface: "inset", title: "Inset", text: "Sunk into the page, for notes and secondary detail." },
];
export default function Surfaces(): ReactElement {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(180px, 1fr))",
gap: "var(--space-md)",
width: "100%",
}}
>
{SURFACES.map(
({
surface,
title,
text,
}: {
readonly surface: CardSurface;
readonly title: string;
readonly text: string;
}): ReactElement => (
<Card key={surface} title={title} surface={surface}>
{text}
</Card>
),
)}
</div>
);
}
Priority
priority (emergency, alarm, warning or caution) widens the card's start edge in the priority's color and puts the
priority's IEC 62923 shape and its word in the header, in the crew's language. The colors, shapes and words come from
the kit's priority mapping. Use it for a card about an alert's subject, such as the system an alarm concerns; the
alert itself belongs in the alert zone and the alert list.
Fire, engine room
Emergency alarmAnchor drag
AlarmBilge pump
WarningService battery
CautionCode
import { Card, type CardPriority } from "@bosunkit/components/card";
import type { ReactElement } from "react";
export const description: string =
"A card for each priority: the edge in its color, with its shape and its word in the header.";
const CARDS: readonly { readonly priority: CardPriority; readonly title: string; readonly text: string }[] = [
{ priority: "emergency", title: "Fire, engine room", text: "Smoke detector 2 since 03:12 UTC." },
{ priority: "alarm", title: "Anchor drag", text: "38 m outside the swing circle." },
{ priority: "warning", title: "Bilge pump", text: "Ran 4 times in the last hour." },
{ priority: "caution", title: "Service battery", text: "12.1 V, charging from the alternator." },
];
export default function Priority(): ReactElement {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(220px, 1fr))",
gap: "var(--space-md)",
width: "100%",
}}
>
{CARDS.map(
(card: { readonly priority: CardPriority; readonly title: string; readonly text: string }): ReactElement => (
<Card key={card.priority} title={card.title} priority={card.priority}>
{card.text}
</Card>
),
)}
</div>
);
}
Integrity
integrity marks how far the data in the card can be trusted, in the same visual language as a
reading (MSC.191 §5.6). A stale card dashes its border in the low-integrity color and
dims its content; a degraded card dashes its border and keeps its content at full strength; an invalid card frames
itself in the invalid color and hatches its content. The state's word sits in the header, with the data's age
(integrityAge, "Stale, 3 min") or the reason (integrityReason, "Degraded, weak bottom lock"), and describes the
region to a screen reader, so the state never rests on color or pattern alone. Mark a card when the whole panel
comes from one sensor; a panel of several readings marks each reading instead.
Position
Speed
Stale, 3 minDepth
Degraded, weak bottom lockWind
Invalid, last seen 5 min agoCode
import { Card } from "@bosunkit/components/card";
import type { ReactElement } from "react";
export const description: string =
"Four navigation panels, one per integrity: fresh, stale for 3 minutes, degraded by a weak bottom lock, and " +
"invalid since the wind sensor was last seen 5 minutes ago.";
export default function Integrity(): ReactElement {
return (
<div
style={{
display: "grid",
gridTemplateColumns: "repeat(auto-fit, minmax(14em, 1fr))",
gap: "var(--space-md)",
width: "100%",
}}
>
<Card title="Position" footer="GPS, 14:05 UTC">
47°17.4′N 004°36.9′W
</Card>
<Card title="Speed" integrity="stale" integrityAge="3 min" footer="Log, 14:02 UTC">
STW 5.8 kn
</Card>
<Card title="Depth" integrity="degraded" integrityReason="weak bottom lock" footer="Sounder, 14:05 UTC">
12.4 m under the keel
</Card>
<Card title="Wind" integrity="invalid" integrityAge="5 min" footer="Masthead, 14:00 UTC">
TWS 18 kn, TWD 245°
</Card>
</div>
);
}
Untitled
A card with no title has no header and no landmark: it frames its content and nothing more.
Code
import { Card } from "@bosunkit/components/card";
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string = "A card with no title: a plain box around its content, with no header or landmark.";
export default function Untitled(): ReactElement {
return (
<Card surface="raised">
<Reading value={12.4} unit="m" label="Depth" source="DST810" fractionDigits={1} />
</Card>
);
}
Props
appearance="metric" gives a compact metric its dedicated inset, rounded surface and anchored comparison
footer. See the metric cards with bar sparklines
for supplied values, units and deltas. The caller owns each quantity and reporting period.
| Prop | Type | Default | Description |
|---|---|---|---|
| appearance | "default" | "metric" | undefined | "default" | Default content frame, or a metric frame with a large figure and an anchored comparison footer. |
| title | ReactNode | undefined | – | The card's title, as display text; it names the card, which is then a region. |
| headingLevel | NestedHeadingLevel | undefined | 3 | The level of the title's heading element, so the card sits in the page's outline; 3 by default. |
| icon | ReactNode | undefined | – | An icon before the title; it is decorative, since the title says what the card holds. |
| actions | ReactNode | undefined | – | Controls at the end of the header, such as a button that opens the card's page. |
| children | ReactNode | undefined | – | The card's content. |
| footer | ReactNode | undefined | – | A row under the content, set off by a rule: a source, a time with its reference, or secondary actions. |
| surface | CardSurface | undefined | "default" | The surface the card sits on: default, raised or inset. |
| priority | CardPriority | undefined | – | The priority of what the card shows, when it concerns an alert: an edge in the priority's color along the start of the card, and the priority's shape and word in the header, so the priority never rests on color alone. |
| integrity | ReadingIntegrity | undefined | "fresh" | How far the data the card shows can be trusted, as Reading marks one value (MSC.191 §5.6): fresh (the
default) is plain; stale dashes the border and dims the content; degraded dashes the border; invalid frames
the card in the invalid color and hatches the content. Each puts its word in the header ("Stale, 3 min"), so the
state never rests on color alone. |
| integrityAge | string | undefined | – | How old the data is, as the caller displays it ("3 min"): written after the word of a stale card, and after the word of an invalid one as the time since it was last seen. |
| integrityReason | string | undefined | – | Why the data is below full trust, in the quality evaluator's words ("weak bottom lock"): written after the word of a degraded or an invalid card, where it takes the place of the age. |
| className | string | undefined | – | Classes added to the root. |
| style | CSSProperties | undefined | – | Inline style on the root, for the caller's layout (width, grid placement). |
| testId | string | undefined | – | Becomes data-testid on the root; the parts are ${testId}--header, --title, --integrity, --body and
--footer. |
| ref | Ref<HTMLElement> | undefined | – | The root element. |
Barometer
Anchor drag
AlarmRelated
- Alert card: one alert, with its acknowledgement.
- Alert card grid: alert cards in columns, grouped by urgency.
- Section heading: the heading over a group of cards.
- Description list: the terms and values a card often holds.