Skip to content
design
Instruments

Readout

A value with an optional setpoint, advice, label, unit and source, laid out vertically or horizontally.

On this page
64
°
54
°
HDG
Fluxgate compass
6.5
6.1
SOG
kn
GPS
4.2
Depth
m
Echosounder
17.0
TWS
kn
Masthead, last calibrated 2025
‒
Sea temp
°C
Transducer
Standby
Radar
Halo 20+
Under way using engine
AIS status
Class B transponder
Anchor watch, armed at 21:40

Autopilot turning to 064° for the Sóller lighthouse; speed is under the 6.5 kn advised for an 18:00 arrival.

Large, medium and horizontal readouts with a setpoint, an advice value, custom icons, hinted zeros, low-integrity and invalid data, an off value, text and a label-only row, on a live feed.
Code
import {
  ObcTextboxFontWeight,
  Readout,
  ReadoutDataQuality,
  ReadoutDirection,
  ReadoutPriority,
  ReadoutSize,
  ReadoutValueType,
} from "@bosunkit/instruments";
import { Gauge, Navigation, Target, Wind } from "lucide-react";
import { useEffect, useState } from "react";
import type { CSSProperties, Dispatch, ReactElement, SetStateAction } from "react";

export const description: string =
  "Althea's navigation readouts on a one-second feed: the autopilot heading closing on its setpoint, speed against the advised passage speed, the depth shoaling under the anchorage limit, and horizontal rows for wind of low integrity, an invalid sensor, a radar on standby, a text status and a label-only row.";

const PANEL_STYLE: CSSProperties = {
  display: "grid",
  gap: "var(--space-lg)",
  padding: "var(--space-lg)",
  borderRadius: "var(--radius-md)",
  background: "var(--container-section-color)",
  width: "100%",
  maxWidth: "40em",
};

/** The three vertical readouts in equal columns, their feet on one line, so their sources line up. */
const HEADLINE_STYLE: CSSProperties = {
  display: "grid",
  gridTemplateColumns: "repeat(3, minmax(0, 1fr))",
  alignItems: "end",
  justifyItems: "center",
  gap: "var(--space-lg)",
};

/** The horizontal rows under a rule, each starting on the same edge. */
const ROWS_STYLE: CSSProperties = {
  display: "grid",
  justifyItems: "start",
  gap: "var(--space-sm)",
  paddingBlockStart: "var(--space-lg)",
  borderBlockStart: "1px solid var(--border-divider-color)",
};

const NOTE_STYLE: CSSProperties = { margin: 0, font: "var(--bk-type-caption)", color: "var(--element-neutral-color)" };

/** A depth in tenths of a meter, floored, so it never shows more water than there is. */
function tenths(depth: number): string {
  const floored: number = Math.floor(depth * 10);
  return `${String(Math.trunc(floored / 10))}.${String(floored % 10)}`;
}

/** The autopilot's course to steer after the crew dialed 10° to starboard for the Sóller lighthouse. */
const HEADING_SETPOINT: number = 64;

export default function Showcase(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);

  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;
  const heading: number = Math.min(HEADING_SETPOINT, 54 + phase * 0.8);
  const speed: number = 6.1 + 0.3 * Math.sin(step / 3);
  const depth: number = 4.2 - phase * 0.05;
  const shallow: boolean = depth < 3.5;
  const trueWind: number = 17 + 2 * Math.sin(step / 2);

  return (
    <div style={PANEL_STYLE}>
      <div style={HEADLINE_STYLE}>
        <Readout
          label="HDG"
          src="Fluxgate compass"
          value={heading}
          hasDegree
          maxDigits={3}
          size={ReadoutSize.large}
          priority={ReadoutPriority.enhanced}
          hasSetpoint
          setpoint={HEADING_SETPOINT}
          setpointIcon={<Navigation size={14} aria-hidden />}
          valueOptions={{ weight: ObcTextboxFontWeight.bold, hintedZeros: true }}
          setpointOptions={{ hintedZeros: true }}
        />
        <Readout
          label="SOG"
          unit="kn"
          src="GPS"
          value={speed}
          fractionDigits={1}
          maxDigits={2}
          size={ReadoutSize.medium}
          hasAdvice
          advice={6.5}
          adviceIcon={<Target size={14} aria-hidden />}
          unitOptions={{ spaceReserver: "kn" }}
        />
        <Readout
          label="Depth"
          unit="m"
          src="Echosounder"
          value={depth}
          fractionDigits={1}
          maxDigits={2}
          size={ReadoutSize.medium}
        />
      </div>
      <div style={ROWS_STYLE}>
        <Readout
          direction={ReadoutDirection.horizontal}
          label="TWS"
          unit="kn"
          src="Masthead, last calibrated 2025"
          value={trueWind}
          fractionDigits={1}
          dataQuality={ReadoutDataQuality.lowIntegrity}
          valueIcon={<Wind size={16} aria-hidden />}
          valueOptions={{ hasIcon: true }}
        />
        <Readout
          direction={ReadoutDirection.horizontal}
          label="Sea temp"
          unit="°C"
          src="Transducer"
          value={null}
          dataQuality={ReadoutDataQuality.invalid}
          hasDegreeSpacer
        />
        <Readout
          direction={ReadoutDirection.horizontal}
          label="Radar"
          src="Halo 20+"
          value={0}
          off
          offText="Standby"
          valueIcon={<Gauge size={16} aria-hidden />}
          valueOptions={{ hasIcon: true }}
        />
        <Readout
          direction={ReadoutDirection.horizontal}
          label="AIS status"
          src="Class B transponder"
          value="Under way using engine"
          valueType={ReadoutValueType.text}
        />
        <Readout direction={ReadoutDirection.horizontal} label="Anchor watch, armed at 21:40" hasValue={false} />
      </div>
      <p style={NOTE_STYLE}>
        {shallow
          ? `Depth ${tenths(depth)} m is under the 3.5 m limit set for this anchorage: slow down and turn toward deeper water to the north.`
          : "Autopilot turning to 064° for the Sóller lighthouse; speed is under the 6.5 kn advised for an 18:00 arrival."}
      </p>
    </div>
  );
}

A readout shows one measured value with the parts around it: a setpoint, an advice value, a label, a unit and the source of the reading. The parts are laid out vertically or horizontally, in three sizes. fractionDigits sets the decimals and maxDigits reserves the width of the integer digits, so the layout does not move as the value changes.

A value of null shows a dash, and valueType="text" shows text as given. dataQuality marks a low-integrity or invalid value, and alert draws an alert frame around the readout. The source can open a picker or a flyout.

Examples

Value

9.9
A value of 9.87 shown to one decimal, with room for three integer digits.
Code
import { Readout } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A value of 9.87 shown to one decimal, with room for three integer digits.";

export default function Value(): ReactElement {
  return (
    <Readout value={9.87} fractionDigits={1} maxDigits={3} />
  );
}

No value

‒
A value that is not available shows a dash, so the layout does not move.
Code
import { Readout } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A value that is not available shows a dash, so the layout does not move.";

export default function NoValue(): ReactElement {
  return (
    <Readout value={null} />
  );
}

A complete reading

Keep the quantity, unit and reference together: a depth below the surface and an under-keel clearance need different labels. Set precision from the measurement and domain policy, and reserve enough digits for the full operating range. Do not resize the value every time it crosses a digit boundary.

dataQuality is supplied by the caller; the component does not decide whether telemetry is stale. Use the application's age and integrity adapter, or Reading for its richer integrity presentation. A source picker needs an actual choice flow and a clear accessible name.

Props

PropTypeDefaultDescription
labelstring | undefined–Name of the quantity, for example "Depth".
unitstring | undefined–Unit shown beside the value, for example "m". Nothing is shown when it is empty.
srcstring | undefined–Name of the source of the reading, for example "Sonar". A blank string shows no source row.
hasValueboolean | undefinedtrueLayout switch: false renders a deliberately value-less (label-only) readout that hugs its remaining parts. For a temporarily missing value keep hasValue and set value to null instead — the dash keeps the value block at full size, so the layout does not shift when data arrives.
valuenumber | string | null | undefinednullThe value; null renders a dash. A number by default, or text when {@link ReadoutProps.valueType} is text.
valueTypeReadoutValueType | undefinedReadoutValueType.numberHow {@link ReadoutProps.value} is interpreted. number (default) formats it via fractionDigits; text renders it verbatim and ignores the numeric format options.
offboolean | undefinedfalseRender the value as offText (e.g. equipment powered down). Affects the value only.
offTextstring | undefined"OFF"Text shown in place of the value when off is true. Available when off==true.
hasSetpointboolean | undefinedfalseShows the setpoint block beside the value.
setpointnumber | undefined–Available when hasSetpoint==true.
hasAdviceboolean | undefinedfalseShows the advice block beside the value.
advicenumber | undefined–Available when hasAdvice==true.
sizeReadoutSize | undefined–Density of the readout: small (default), medium or large. Larger sizes use larger digits.
priorityReadoutPriority | undefined–Color emphasis of the value: regular (default) or enhanced, the in-command accent color.
directionReadoutDirection | undefined–vertical (default) stacks the blocks above the label and unit. horizontal puts every segment on one row.
stackingReadoutStacking | undefined–Available when direction==vertical.
alignmentReadoutAlignment | undefined–Available when direction==vertical && stacking==stacked.
hasDegreeboolean | undefinedfalseAppends a degree sign (°) after each number, for headings and angles.
hasDegreeSpacerboolean | undefinedfalseAvailable when hasDegree==false.
fractionDigitsnumber | undefined0Also formats the numeric setpoint / advice blocks, which stay numeric even when the value is text. Available when valueType==number || hasSetpoint || hasAdvice.
maxDigitsnumber | undefined0Same availability as {@link ReadoutProps.fractionDigits}.
dataQualityReadoutDataQuality | undefined–Marks the value as low-integrity or invalid with an outline.
alertboolean | AlertFrameConfig | undefinedfalseReadout-level alert frame. true normalizes to a default frame; an {@link AlertFrameConfig} object configures it; false renders none.
valueOptionsReadoutValueOptions | undefined–Available when hasValue==true.
setpointOptionsReadoutSetpointOptions | undefined–Available when hasSetpoint==true.
adviceOptionsReadoutAdviceOptions | undefined–Available when hasAdvice==true.
unitOptionsReadoutReserverOptions | undefined–Available when unit!=''.
srcOptionsReadoutSourceOptions | undefined–Available when src!=''.
showDebugOverlayboolean | undefinedfalseDevelopment aid: outline the readout building blocks (red), the degree columns (blue) and the degree spacer (green) so reserver widths / alignment are visible. Off by default.
valueIconReactNode | undefined–Icon before the value (the upstream value-icon slot). Available when valueOptions.hasIcon==true.
setpointIconReactNode | undefined–Overrides the default setpoint icon (the upstream setpoint-icon slot).
adviceIconReactNode | undefined–Overrides the default advice icon (the upstream advice-icon slot).
srcPickerContentReactNode | undefined–Source picker pop-over content (the upstream src-picker-content slot). Rendered below the source row while the picker is open. The upstream element parsed slotted obc-navigation-items into a context menu and fired source-change; the React translation renders this content directly, so consumers attach their own selection handlers to it.
onSourceFlyoutClick((src: string) => void) | undefined–Fired when the source row is clicked while srcOptions.interaction == 'flyout'.
classNamestring | undefined–Class name added to the root element.