Reading
A live value with its unit, trend, age and source, and the trust it carries.
On this page
Code
import { Reading, type ReadingTrend } from "@bosunkit/components/reading";
import { ReadoutDirection, ReadoutPriority, ReadoutSize } from "@bosunkit/instruments";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A helm panel on a live one-second feed: depth large and enhanced with its trend, speed and wind in medium, and " +
"a row of small readings that are degraded, stale, lost and horizontal.";
/** Seconds since the example started; every value is derived from it. */
function useFeedSeconds(): number {
const [seconds, setSeconds]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const timer: ReturnType<typeof setInterval> = setInterval((): void => {
setSeconds((current: number): number => current + 1);
}, 1000);
return (): void => {
clearInterval(timer);
};
}, []);
return seconds;
}
/** The trend between two samples, steady within the given band. */
function trendOf(previous: number, current: number, band: number): ReadingTrend {
if (current - previous > band) {
return "rising";
}
return previous - current > band ? "falling" : "steady";
}
/** Depth over a sand bar on the approach to Sóller: shoaling to about 6 m and deepening again, once a minute. */
function depthAt(seconds: number): number {
return 10.5 + Math.cos((seconds / 60) * 2 * Math.PI) * 4.4;
}
function speedAt(seconds: number): number {
return 5.8 + Math.sin(seconds / 5) * 0.4;
}
function windAt(seconds: number): number {
return 17 + Math.sin(seconds / 9) * 3 + (seconds % 7 === 0 ? 2.5 : 0);
}
export default function Showcase(): ReactElement {
const seconds: number = useFeedSeconds();
const depth: number = depthAt(seconds);
const speed: number = speedAt(seconds);
const wind: number = windAt(seconds);
return (
<div style={{ display: "flex", flexDirection: "column", gap: "var(--space-xl)", width: "100%" }}>
<div style={{ display: "flex", gap: "var(--space-2xl)", flexWrap: "wrap", alignItems: "flex-end" }}>
<Reading
value={depth}
unit="m"
label="Depth"
source="DST810"
fractionDigits={1}
maxDigits={2}
trend={trendOf(depthAt(seconds - 1), depth, 0.05)}
size={ReadoutSize.large}
priority={ReadoutPriority.enhanced}
/>
<Reading
value={speed}
unit="kn"
label="SOG"
source="GPS · 10Hz"
fractionDigits={1}
trend={trendOf(speedAt(seconds - 1), speed, 0.05)}
size={ReadoutSize.medium}
/>
<Reading
value={wind}
unit="kn"
label="AWS"
source="WS320"
fractionDigits={1}
maxDigits={2}
trend={trendOf(windAt(seconds - 1), wind, 0.2)}
size={ReadoutSize.medium}
/>
</div>
<div
style={{
display: "flex",
gap: "var(--space-xl)",
flexWrap: "wrap",
alignItems: "flex-start",
paddingTop: "var(--space-lg)",
borderTop: "1px solid var(--border)",
}}
>
<Reading
value={218}
unit="°T"
label="HDG"
source="Compass"
integrity="degraded"
reason="heading frozen for 40 s"
size={ReadoutSize.small}
/>
<Reading
value={19.4}
unit="°C"
label="Sea temp"
source="DST810"
fractionDigits={1}
integrity="stale"
ageSeconds={95 + seconds}
size={ReadoutSize.small}
/>
<Reading
value={null}
unit="°C"
label="Coolant"
source="J1939"
ageSeconds={480 + seconds}
size={ReadoutSize.small}
/>
<Reading
value={1008}
unit="hPa"
label="Baro"
source="BME280"
trend="falling"
size={ReadoutSize.small}
direction={ReadoutDirection.horizontal}
/>
</div>
</div>
);
}
Reading is the way a page prints a live value. It composes the instruments' Readout — the value, its unit,
its label and its source — and adds the data-trust treatment the HMI rules ask for, so a number never appears
without the unit it is in, where it came from, and how far it can be trusted. The states themselves, and what
the crew does about them, belong to Data integrity. A reading that knows its
source opens it, with its trust, from the value: a resting mouse, a press or a tap opens the
reading detail, so a gloved hand at the helm reaches it as a mouse does.
Examples
Integrity states
Switch themes in the sidebar to read the four states in Night, Dusk, Day and Bright.
Fresh with source
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string = "A fresh depth with its unit, its label and the instrument it comes from.";
export default function FreshWithSource(): ReactElement {
return <Reading value={12.4} unit="m" label="Depth" source="DST810" fractionDigits={1} />;
}
A fresh reading renders plainly.
Stale
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string =
"A stale depth, dimmed and carrying an age badge for the three minutes since it was measured.";
export default function Stale(): ReactElement {
return (
<Reading value={12.4} unit="m" label="Depth" source="DST810" fractionDigits={1} integrity="stale" ageSeconds={190} />
);
}
A stale reading dims and carries an amber age badge in the compact form the rest of the system uses.
Degraded
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string =
"A degraded heading, current and at full brightness, with the reason the evaluator distrusts it.";
export default function Degraded(): ReactElement {
return <Reading value={225} unit="°T" label="HDG" source="NMEA" integrity="degraded" reason="heading frozen" />;
}
A degraded reading keeps its value at full brightness — the data is current, only its quality is advisory — and states the reason the evaluator distrusts it.
Invalid
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string =
"A lost depth, which keeps its place, dashes the value and says how long ago it was seen.";
export default function Invalid(): ReactElement {
return <Reading value={null} unit="m" label="Depth" source="DST810" ageSeconds={480} />;
}
An invalid reading marks the value red, keeps the last known value visible and states how long ago it was seen.
Trend
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string = "A rising wind speed, with the direction as an arrow beside the value.";
export default function Trend(): ReactElement {
return <Reading value={18.2} unit="kn" label="AWS" source="WS320" fractionDigits={1} trend="rising" />;
}
The trend arrow is a mark beside the value, and the direction is named in the accessible label with it, so the arrow never carries the meaning on its own.
Not a number
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string =
"A value that is not a number, which takes the same lost treatment as an absent one.";
export default function NotANumber(): ReactElement {
return <Reading value={Number.NaN} fractionDigits={1} unit="m" label="Depth" ageSeconds={180} />;
}
Absent
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string = "A value that is null, which the reading never renders blank.";
export default function Absent(): ReactElement {
return <Reading value={null} fractionDigits={1} unit="m" label="Depth" ageSeconds={180} />;
}
A value that is null, undefined, NaN or infinite always takes the invalid treatment, with the readout's
width-stable dash, so the component cannot render blank and cannot present a lost feed as fresh.
Written as text
Code
import { Reading } from "@bosunkit/components/reading";
import type { ReactElement } from "react";
export const description: string =
"A value that is not one number, a position in chart notation, three minutes old: written as given, with the " +
"same stale treatment as any reading.";
export default function Position(): ReactElement {
return <Reading value="50° 45.912′ N 001° 17.964′ W" label="Position" integrity="stale" ageSeconds={180} />;
}
A value that is not one number, such as a position in chart notation, is given as text and shown as written, with
fractionDigits ignored; it takes every integrity state as a number does, and empty text counts as lost.
Age
An age reads as 45 s, 5 min, 2 h or 3 d, SN.1/Circ.243's symbols, the same in every language, floored to the
unit it names: a reading is not a minute old until a full minute has passed. Minutes are never m, the meter's symbol,
which beside a depth would read as a length. An age no one knows reads as stale rather than a number, and a negative age clamps to
zero. A degraded reading is never captioned with an age — it is flowing, and an age would say it is lost.
Size and emphasis
The size forwards to the readout and belongs to the surface the reading sits on, not to the value's importance.
enhanced raises the value's color emphasis for the few readings a watchkeeper is steering by; the direction lays
the stack out vertically or horizontally for the space it has.
Motion
By default a reading rises in when the caller adds it inside a MotionPresence and fades out when removed; one
there on the first render does not move. motion takes the kit's motion prop: true
does the same outside a presence, and false turns it off. A new value never moves, whatever motion holds: it
jumps to the latest measurement, so the screen never shows a depth that was not measured. The reading is named with
its value from the first frame, and under reduced motion it only fades in.
Code
import { Button } from "@bosunkit/components/button";
import { MotionPresence } from "@bosunkit/components/motion";
import { Reading } from "@bosunkit/components/reading";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"A sea temperature reading that rises in when its sensor reports and fades out when it is removed, beside a depth " +
"whose new values jump into place without moving.";
const DEPTHS: readonly number[] = [8.4, 8.1, 7.6, 7.2];
export default function Motion(): ReactElement {
const [reporting, setReporting]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
const [sample, setSample]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
return (
<div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "var(--space-md)" }}>
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-sm)" }}>
<Button
size="sm"
onClick={(): void => {
setReporting((current: boolean): boolean => !current);
}}
>
{reporting ? "Remove the sensor" : "Fit the sensor"}
</Button>
<Button
size="sm"
onClick={(): void => {
setSample((current: number): number => (current + 1) % DEPTHS.length);
}}
>
Next depth reading
</Button>
</div>
<div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-lg)", minHeight: 64 }}>
<Reading value={DEPTHS[sample]} unit="m" label="Depth" source="DST810" fractionDigits={1} />
<MotionPresence>
{reporting && (
<Reading key="sea" value={16.8} unit="°C" label="Sea" source="DST810" fractionDigits={1} />
)}
</MotionPresence>
</div>
</div>
);
}
Highlight
A reading highlights its update only when the caller asks, with motion={{ change: "highlight" }}. Its background
tints for the slow motion duration when a new value arrives, at most once every 2
seconds, so a fast feed does not flicker; the value itself jumps at once and never rolls. Under reduced motion it
never highlights.
Code
import { Reading } from "@bosunkit/components/reading";
import { useEffect, useState, type Dispatch, type ReactElement, type SetStateAction } from "react";
export const description: string =
"Speed over ground updating every half second. Asked for a highlight, it tints its background on an update at most " +
"once every 2 seconds, so the fast feed does not flicker, and the value always jumps to the latest reading.";
export default function Highlight(): ReactElement {
const [sample, setSample]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
useEffect((): (() => void) => {
const timer: ReturnType<typeof setInterval> = setInterval((): void => {
setSample((current: number): number => current + 1);
}, 500);
return (): void => {
clearInterval(timer);
};
}, []);
const speed: number = 6.2 + 0.3 * Math.sin(sample / 3);
return (
<Reading
value={speed}
unit="kn"
label="SOG"
source="GPS"
fractionDigits={1}
maxDigits={2}
motion={{ change: "highlight" }}
/>
);
}
Props
| Prop | Type | Default | Description |
|---|---|---|---|
| valuerequired | number | string | null | undefined | – | The live value: a number, formatted to fractionDigits, or text for a value that is not one number (a position
in chart notation), shown as written. null, undefined or empty text means the reading is lost: the Reading
renders the Readout's width-stable dash placeholder with the invalid treatment — never blank (Maritime HMI rule 5). |
| unit | string | undefined | – | Measurement unit rendered small beside the value (for example "m", "kn"). |
| label | string | undefined | – | What is being read (for example "Depth", "SOG"). Also opens the aria-label. |
| source | string | undefined | – | Per-value source tag (for example "GPS", "LOG", "IMU · 10Hz") — part of the value-stack anatomy. |
| fractionDigits | number | undefined | 0 | Fraction digits the value is formatted to (default 0). |
| maxDigits | number | undefined | 0 | Integer digits to reserve width for, so the layout never shifts (default 0). |
| integrity | ReadingIntegrity | undefined | "fresh" | Data-trust state (default "fresh"). A missing value always renders as "invalid". |
| ageSeconds | number | undefined | – | Age of the reading in seconds — drives the stale badge and the invalid "last seen" text. |
| reason | string | undefined | – | Why integrity is below fresh, in the quality evaluator's words ("no GPS fix", "heading frozen"). Shown as the caption for degraded and invalid readings — a degraded-but-flowing reading must never be captioned with a staleness age. |
| trend | ReadingTrend | undefined | – | Quantified trend, rendered as a subtle aria-hidden arrow (▲/▼/→) beside the value. |
| size | ReadoutSize | undefined | – | Density/size scale, forwarded to the Readout. |
| direction | ReadoutDirection | undefined | – | Flow direction (vertical/horizontal), forwarded to the Readout. |
| priority | ReadoutPriority | undefined | – | Color emphasis (regular/enhanced), forwarded to the Readout. |
| className | string | undefined | – | Extra class names appended to the .reading root. |
| motion | MotionProp | undefined | – | How the reading moves. By default, inside a MotionPresence where the caller adds and removes it, it rises in
and fades out; one there on the first render does not move. true does the same outside a presence too; a preset
(fade, rise, scale) or { enter, exit, change } chooses; false turns it off.
A new value jumps to the latest measurement and never rolls; change: "highlight" tints the background briefly
when it arrives, at most once every 2 s, and never under reduced motion. A pop, a fade on change and attention
are ignored. |
| ref | Ref<HTMLDivElement> | undefined | – | The reading's root element. |
Related
- Data integrity — the four states, and what the crew does about each one.
- Instruments — the readouts and gauges a reading is built from.
- Accessibility — the accessible label, the target sizes and the contrast floor.
- Alarms and severity — the alerts a value escalates into, which this component does not raise.