Skip to content
design
Components

Card

A bordered surface that holds related content under a header, with an optional footer and a priority edge.

On this page

Navigation

GPS and DST810, 14:05 UTC

Bilge pump

Warning

Ran 4 times in the last hour, 6 min in all. Check the stern gland and the hose clamps.

Float switch, forward bilge, since 13:12 UTC

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.

Three cards from a vessel overview: navigation raised, the bilge pump marked as a warning, and the watch notes sunk into the page.
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.

01One subject per cardA card holds one system or one record, and its title names it, so the crew find it by name and a screen reader lists it as a region.
02Priority in wordsA card about an alert takes the priority: an edge in its color, and its shape and word in the header, so the priority never rests on color alone.
03Surfaces, not shadowsDefault, raised and inset are three token surfaces, so a card reads the same in Night, Dusk, Day and Bright.

Examples

Titled

Tanks

Diesel 212 L, water 365 L, holding 18 %.
A card with a title, which names it as a region.
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>
  );
}

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

Channel 16 and 12 by dual watch; last DSC call 09:41 UTC from Brest Port Control.
An icon before the title and an action at the end of the header.
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>
  );
}

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

BME280 in the cabin, 14:05 UTC
A footer under a rule, holding where the value comes from and when.
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

On the page, for most content.

Raised

Above the page, for the panel the crew is working in.

Inset

Sunk into the page, for notes and secondary detail.
The default, raised and inset surfaces side by side.
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 alarm
Smoke detector 2 since 03:12 UTC.

Anchor drag

Alarm
38 m outside the swing circle.

Bilge pump

Warning
Ran 4 times in the last hour.

Service battery

Caution
12.1 V, charging from the alternator.
A card for each priority: the edge in its color, with its shape and its word in the header.
Code
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

47°17.4′N 004°36.9′W
GPS, 14:05 UTC

Speed

Stale, 3 min
STW 5.8 kn
Log, 14:02 UTC

Depth

Degraded, weak bottom lock
12.4 m under the keel
Sounder, 14:05 UTC

Wind

Invalid, last seen 5 min ago
TWS 18 kn, TWD 245°
Masthead, 14:00 UTC
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.
Code
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.

A card with no title: a plain box around its content, with no header or landmark.
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.

PropTypeDefaultDescription
appearance"default" | "metric" | undefined"default"Default content frame, or a metric frame with a large figure and an anchored comparison footer.
titleReactNode | undefined–The card's title, as display text; it names the card, which is then a region.
headingLevelNestedHeadingLevel | undefined3The level of the title's heading element, so the card sits in the page's outline; 3 by default.
iconReactNode | undefined–An icon before the title; it is decorative, since the title says what the card holds.
actionsReactNode | undefined–Controls at the end of the header, such as a button that opens the card's page.
childrenReactNode | undefined–The card's content.
footerReactNode | undefined–A row under the content, set off by a rule: a source, a time with its reference, or secondary actions.
surfaceCardSurface | undefined"default"The surface the card sits on: default, raised or inset.
priorityCardPriority | 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.
integrityReadingIntegrity | 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.
integrityAgestring | 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.
integrityReasonstring | 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.
classNamestring | undefined–Classes added to the root.
styleCSSProperties | undefined–Inline style on the root, for the caller's layout (width, grid placement).
testIdstring | undefined–Becomes data-testid on the root; the parts are ${testId}--header, --title, --integrity, --body and --footer.
refRef<HTMLElement> | undefined–The root element.
Do

Barometer

1009 hPa, falling 3 hPa in 3 hr
BME280 in the cabin, 14:05 UTC
Name the card after its subject, and put the source and the time in its footer.
Don't

Anchor drag

Alarm
38 m outside the swing circle
Don't raise an alert in a card. An alert belongs in the alert zone and the alert list, where the crew acknowledge it one at a time.