Skip to content
design
Instruments

Readout list

A list that lines up the columns of its readout list items.

On this page

Passage to Sóller

HDG
64
°
64
°
Fluxgate
COG
68
°
GPS
SOG
6.5
6.1
kn
GPS
Depth
8.4
m
Echosounder
TWS
17.6
kn
Masthead
Waypoint
Cap de Formentor
Route

Yanmar 4JH110

Engine speed
2280
rpm
J1939
Coolant
83
°C
J1939
Fuel rate
3.2
L/h
J1939
Oil pressure
‒
bar
Sender
Generator
Off
kW
Panel
Next service in 38 engine hours
Two live lists whose columns keep their place as the depth grows to three digits, with degree spacers, a text row, a heading inside the list, every block type and a switch that outlines the reserved columns.
Code
import { ReadoutDataQuality, ReadoutList, ReadoutListItem, ReadoutValueType } from "@bosunkit/instruments";
import { Label } from "@bosunkit/components/label";
import { Switch } from "@bosunkit/components/switch";
import { Compass, Fuel, Gauge, Thermometer, Waves, Wind } from "lucide-react";
import { useEffect, useId, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";

export const description: string =
  "Two of Althea's lists on a one-second feed: the depth grows from one to three digits off the shelf and every " +
  "column keeps its place, degree rows give the others a spacer, rows without an icon keep the icon's column, a " +
  "text value shortens to fit its card, a heading inside the list renders as given, and a switch outlines the " +
  "columns the list reserves.";

const PANEL_STYLE: CSSProperties = {
  display: "grid",
  gap: 6,
  padding: 12,
  borderRadius: 10,
  background: "var(--container-section-color)",
  minWidth: 0,
};
const HEADING_STYLE: CSSProperties = {
  margin: "0 0 4px",
  fontSize: 12,
  fontWeight: 600,
  color: "var(--element-neutral-color)",
};

export default function Showcase(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  const switchId: string = useId();
  const [showColumns, setShowColumns]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);

  useEffect((): (() => void) => {
    const timer: number = window.setInterval((): void => {
      setStep((previous: number): number => previous + 1);
    }, 1000);
    return (): void => {
      window.clearInterval(timer);
    };
  }, []);

  const phase: number = step % 30;
  // Off the Mallorca shelf the bottom drops from 8 m to over 180 m in half a minute of the feed.
  const depth: number = phase < 25 ? 8.4 * Math.pow(1.13, phase) : 8.4;
  const heading: number = 64 + 2 * Math.sin(step / 3);
  const courseOverGround: number = heading + 4;
  const speed: number = 6.1 + 0.3 * Math.sin(step / 2);
  const rpm: number = 2280 + 40 * Math.sin(step / 4);
  const coolant: number = 83 + 2 * Math.sin(step / 5);
  const fuelRate: number = 3.2 + 0.2 * Math.sin(step / 3);

  return (
    <div style={{ display: "grid", gap: 12, width: "100%", maxWidth: 620 }}>
      <div style={{ display: "grid", gridTemplateColumns: "repeat(auto-fit, minmax(270px, 1fr))", gap: 12 }}>
        <div style={PANEL_STYLE}>
          <ReadoutList showDebugOverlay={showColumns}>
            <p style={HEADING_STYLE}>Passage to Sóller</p>
            <ReadoutListItem
              label="HDG"
              hasLeadingIcon
              leadingIcon={<Compass size={16} aria-hidden />}
              value={heading}
              hasDegree
              maxDigits={3}
              src="Fluxgate"
              hasSetpoint
              setpoint={64}
            />
            <ReadoutListItem label="COG" value={courseOverGround} hasDegree maxDigits={3} src="GPS" />
            <ReadoutListItem label="SOG" value={speed} fractionDigits={1} unit="kn" src="GPS" hasAdvice advice={6.5} />
            <ReadoutListItem
              label="Depth"
              hasLeadingIcon
              leadingIcon={<Waves size={16} aria-hidden />}
              value={depth}
              fractionDigits={1}
              maxDigits={3}
              unit="m"
              src="Echosounder"
            />
            <ReadoutListItem
              label="TWS"
              hasLeadingIcon
              leadingIcon={<Wind size={16} aria-hidden />}
              value={17.6}
              fractionDigits={1}
              unit="kn"
              src="Masthead"
              dataQuality={ReadoutDataQuality.lowIntegrity}
            />
            <ReadoutListItem label="Waypoint" value="Cap de Formentor" valueType={ReadoutValueType.text} src="Route" />
          </ReadoutList>
        </div>
        <div style={PANEL_STYLE}>
          <ReadoutList showDebugOverlay={showColumns}>
            <p style={HEADING_STYLE}>Yanmar 4JH110</p>
            <ReadoutListItem
              label="Engine speed"
              hasLeadingIcon
              leadingIcon={<Gauge size={16} aria-hidden />}
              value={rpm}
              maxDigits={4}
              unit="rpm"
              src="J1939"
            />
            <ReadoutListItem
              label="Coolant"
              hasLeadingIcon
              leadingIcon={<Thermometer size={16} aria-hidden />}
              value={coolant}
              unit="°C"
              src="J1939"
            />
            <ReadoutListItem
              label="Fuel rate"
              hasLeadingIcon
              leadingIcon={<Fuel size={16} aria-hidden />}
              value={fuelRate}
              fractionDigits={1}
              unit="L/h"
              src="J1939"
            />
            <ReadoutListItem
              label="Oil pressure"
              value={null}
              unit="bar"
              src="Sender"
              dataQuality={ReadoutDataQuality.invalid}
            />
            <ReadoutListItem label="Generator" value={0} off offText="Off" unit="kW" src="Panel" />
            <ReadoutListItem label="Next service in 38 engine hours" hasValue={false} />
          </ReadoutList>
        </div>
      </div>
      <div style={{ display: "flex", alignItems: "center", gap: 8 }}>
        <Switch id={switchId} checked={showColumns} onCheckedChange={setShowColumns} />
        <Label htmlFor={switchId} style={{ marginBottom: 0, cursor: "pointer" }}>
          Outline the reserved columns
        </Label>
      </div>
    </div>
  );
}

A readout list groups readout list items and lines up their columns. It reads each row and passes the same reserved widths to all of them: the longest unit, the widest value, setpoint and advice, and the longest source. No row is clipped by a wider one, and the columns line up from row to row. Alignment is always on.

Rows must be direct children of the list. Other children are rendered as given.

Examples

Depth and heading

Depth
123.4
m
HDG
182
°
Two rows in one list, which reserves shared widths so their values line up.
Code
import { ReadoutList, ReadoutListItem } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "Two rows in one list, which reserves shared widths so their values line up.";

export default function DepthAndHeading(): ReactElement {
  return (
    <ReadoutList>
      <ReadoutListItem label="Depth" value={123.4} fractionDigits={1} unit="m" />
      <ReadoutListItem label="HDG" value={182} hasDegree />
    </ReadoutList>
  );
}

Shared columns and grouping

Put ReadoutListItem elements directly inside the list so their reservations participate in alignment. A wrapper around an item is not an aligned row. Group readings with compatible context and use a heading when the source, reference or equipment changes.

Test a transition across a digit boundary, missing values, the longest unit and source, and narrow layouts. Reserve the expected range to reduce column movement; alignment alone does not guarantee that a dense list fits a phone. Split the group or choose a stacked layout when labels and units cannot remain readable.

Props

PropTypeDefaultDescription
showDebugOverlayboolean | undefinedfalseDevelopment aid: outline each row's readout building blocks (red), degree columns (blue) and degree spacer (green) so the reserved column widths are visible. Propagated to every row. Off by default.
childrenReactNode | undefined–The ReadoutListItem rows (the upstream default slot). Direct ReadoutListItem children are auto-aligned; any other children render untouched. The upstream element also found rows nested inside wrapper elements via DOM traversal — the React translation aligns direct children only, so keep rows immediate children of the list.
classNamestring | undefined–Class name added to the root element.