Skip to content
design
Instruments

Compass

The heading card with heading and course over ground arrows, wind and current arrows, and a rate-of-turn indicator.

On this page
ESW
228
HDG
DEG
232
COG
DEG
57
ROT
°/min
Autopilot, north up: readouts, rate-of-turn bar by side, advice and caution sectors
ESWN
Helm, heading up: the vessel, needles and circling rate-of-turn dots
Chart table repeater, course up, while the heading sensor starts
A passage under autopilot on a live feed, on a north-up, a heading-up and a course-up card.
Code
import {
  AdviceType,
  CogArrowStyle,
  Compass,
  CompassDirection,
  CompassPriorityElement,
  CompassReadoutSource,
  HdgArrowStyle,
  InstrumentState,
  Priority,
  RotPosition,
  RotType,
  VesselImage,
  type AngleAdvice,
  type CompassCenterReadout,
} from "@bosunkit/instruments";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "A passage under autopilot on a live feed: the heading wanders around the setpoint and into a caution sector, " +
  "the rate of turn, course over ground, wind and current follow, and the setpoint is adjusted and confirmed " +
  "every eight seconds, on a north-up, a heading-up and a course-up card.";

const CENTER_READOUTS: CompassCenterReadout[] = [
  { source: CompassReadoutSource.hdg },
  { source: CompassReadoutSource.cog },
  { source: CompassReadoutSource.rot, fractionDigits: 0 },
];

/** Port of the setpoint is open water; the starboard sector closes on the shoal off the headland. */
const ADVICES: AngleAdvice[] = [
  { minAngle: 196, maxAngle: 214, type: AdviceType.advice, hinted: true },
  { minAngle: 236, maxAngle: 262, type: AdviceType.caution, hinted: true },
];

const PANEL: CSSProperties = {
  display: "flex",
  flexWrap: "wrap",
  alignItems: "flex-end",
  justifyContent: "center",
  gap: 28,
  width: "100%",
  maxWidth: 680,
};

const FIGURE: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "center", gap: 8, margin: 0 };
const CAPTION: CSSProperties = { fontSize: 13, color: "var(--text-dim)", textAlign: "center", maxWidth: 220 };

/** The heading the feed reports at a given second: a slow swing with a faster yaw on top. */
function headingAt(second: number): number {
  return 228 + 9 * Math.sin(second / 20) + 2 * Math.sin(second / 4);
}

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

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

  const heading: number = (headingAt(second) + 360) % 360;
  const rateOfTurn: number = (headingAt(second + 0.5) - headingAt(second - 0.5)) * 60;
  const courseOverGround: number = (heading + 4 + Math.sin(second / 5) + 360) % 360;
  const windFrom: number = 300 + 6 * Math.sin(second / 11);
  const windKnots: number = 18 + 3 * Math.sin(second / 4);

  // The setpoint alternates between 225 and 232 degrees. In the two seconds before each change the helmsman is
  // turning the knob, so the new value shows as a second marker; on release the marker slides across.
  const phase: number = second % 8;
  const confirmed: number = Math.floor(second / 8) % 2 === 0 ? 225 : 232;
  const next: number = confirmed === 225 ? 232 : 225;
  const adjusting: boolean = phase >= 6;

  return (
    <div style={PANEL}>
      <figure style={FIGURE}>
        <Compass
          heading={heading}
          courseOverGround={courseOverGround}
          headingSetpoint={confirmed}
          newHeadingSetpoint={adjusting ? next : undefined}
          touching={adjusting}
          autoAtHeadingSetpoint
          autoAtHeadingSetpointDeadband={3}
          headingSetpointAtZeroDeadband={0.5}
          animateSetpoint
          headingAdvices={ADVICES}
          currentWindSpeedKnots={windKnots}
          windFromDirection={windFrom}
          currentSpeed={2}
          currentFromDirection={160}
          rateOfTurnDegreesPerMinute={rateOfTurn}
          rotType={RotType.bar}
          rotPosition={RotPosition.scale}
          rotMaxValue={60}
          rotArcExtent={45}
          rotPortStarboard
          rotAtZeroDeadband={0.5}
          centerReadouts={CENTER_READOUTS}
          hdgArrowStyle={HdgArrowStyle.vector}
          cogArrowStyle={CogArrowStyle.velocityVector}
          direction={CompassDirection.NorthUp}
          priority={Priority.enhanced}
          priorityElements={[
            CompassPriorityElement.hdg,
            CompassPriorityElement.cog,
            CompassPriorityElement.rot,
            CompassPriorityElement.wind,
          ]}
          showLabels
          faceDiameter={250}
        />
        <figcaption style={CAPTION}>
          Autopilot, north up: readouts, rate-of-turn bar by side, advice and caution sectors
        </figcaption>
      </figure>

      <figure style={FIGURE}>
        <Compass
          heading={heading}
          courseOverGround={courseOverGround}
          headingSetpoint={confirmed}
          newHeadingSetpoint={adjusting ? next : undefined}
          touching={adjusting}
          autoAtHeadingSetpoint
          animateSetpoint
          headingAdvices={ADVICES}
          currentWindSpeedKnots={windKnots}
          windFromDirection={windFrom}
          currentSpeed={2}
          currentFromDirection={160}
          rateOfTurnDegreesPerMinute={rateOfTurn}
          rotDotAnimationFactor={12}
          rotType={RotType.dots}
          rotPosition={RotPosition.innerCircle}
          vesselImage={VesselImage.genericTop}
          hdgArrowStyle={HdgArrowStyle.needle}
          cogArrowStyle={CogArrowStyle.needle}
          direction={CompassDirection.HeadingUp}
          priority={Priority.regular}
          showLabels
          tickmarksInside
          faceDiameter={170}
        />
        <figcaption style={CAPTION}>Helm, heading up: the vessel, needles and circling rate-of-turn dots</figcaption>
      </figure>

      <figure style={FIGURE}>
        <Compass
          heading={heading}
          courseOverGround={courseOverGround}
          headingSetpoint={confirmed}
          headingSetpointOverride
          atHeadingSetpoint={false}
          hdgArrowStyle={HdgArrowStyle.beamLine}
          cogArrowStyle={CogArrowStyle.arrowHead}
          direction={CompassDirection.CourseUp}
          state={InstrumentState.loading}
          faceDiameter={110}
        />
        <figcaption style={CAPTION}>Chart table repeater, course up, while the heading sensor starts</figcaption>
      </figure>
    </div>
  );
}

The compass is the instrument the watchkeeper steers by. A card turns under two arrows, one for the heading the bow points along and one for the course over ground the vessel makes good, and a setpoint marker shows the heading that is set. Around the ring it carries the wind arrow, the current arrow and a rate-of-turn indicator, so one instrument answers where the bow points, where the vessel goes and what pushes it. direction sets the card to north up, heading up or course up. It ports obc-compass; the overview lists the other instruments.

Examples

North up

ESW
The card with north at the top, the heading and course over ground arrows, the wind and current arrows, and the rate-of-turn dots.
Code
import { Compass, CompassDirection, VesselImage } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "The card with north at the top, the heading and course over ground arrows, the wind and current arrows, and the rate-of-turn dots.";

export default function NorthUp(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <Compass
        heading={47}
        courseOverGround={52}
        currentWindSpeedKnots={14}
        windFromDirection={310}
        currentSpeed={1.2}
        currentFromDirection={190}
        rateOfTurnDegreesPerMinute={8}
        direction={CompassDirection.NorthUp}
        vesselImage={VesselImage.genericTop}
        showLabels
      />
    </div>
  );
}

With readouts

ESW
47
HDG
DEG
52
COG
DEG
8
ROT
°/min
Heading, course over ground and rate of turn as readouts in the center, on a face pinned to 240 pixels.
Code
import { Compass, CompassDirection, CompassReadoutSource, type CompassCenterReadout } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "Heading, course over ground and rate of turn as readouts in the center, on a face pinned to 240 pixels.";

const CENTER_READOUTS: CompassCenterReadout[] = [
  { source: CompassReadoutSource.hdg },
  { source: CompassReadoutSource.cog },
  { source: CompassReadoutSource.rot },
];

export default function WithReadouts(): ReactElement {
  return (
    <Compass
      heading={47}
      courseOverGround={52}
      rateOfTurnDegreesPerMinute={8}
      direction={CompassDirection.NorthUp}
      centerReadouts={CENTER_READOUTS}
      showLabels
      faceDiameter={240}
    />
  );
}

Small face

ESW
A compass on a face pinned to 180 pixels, which sizes itself and needs no surrounding box.
Code
import { Compass } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string =
  "A compass on a face pinned to 180 pixels, which sizes itself and needs no surrounding box.";

export default function SmallFace(): ReactElement {
  return <Compass heading={47} courseOverGround={52} rateOfTurnDegreesPerMinute={8} showLabels faceDiameter={180} />;
}

Reference and layers

Keep heading, course over ground and the setpoint in a declared reference, such as true or magnetic. The component receives angles; the application performs any conversion and labels the reference. Heading is where the bow points, while course over ground describes movement over the ground. Do not substitute one for the other when a source is lost or the vessel is nearly stationary.

Choose north-up, heading-up or course-up deliberately and show that choice near the instrument. Add wind, current and rate of turn only when their sources and references are known. A dense card needs enough physical space for its labels; use Heading when the extra layers do not support the task. Source age and integrity belong in the surrounding reading or instrument panel.

Props

PropTypeDefaultDescription
headingnumber | undefined–The current heading of the vessel in degrees; unset or not finite, no heading arrow and no vessel are drawn.
courseOverGroundnumber | undefined–The current course over ground in degrees; unset or not finite, no course arrow is drawn.
headingSetpointnumber | null | undefinednullThe set point for the heading in degrees.
newHeadingSetpointnumber | undefined–New setpoint being adjusted (renders a second, focus-state marker).
atHeadingSetpointboolean | undefinedfalseManual at-setpoint flag; used when autoAtHeadingSetpoint is false.
headingSetpointAtZeroDeadbandnumber | undefined0.5Zero-snap deadband for the setpoint marker, in degrees.
headingSetpointOverrideboolean | undefinedfalseDerive the setpoint color from priority even when loading/off.
autoAtHeadingSetpointboolean | undefinedtrueEnables automatic at-setpoint calculation.
autoAtHeadingSetpointDeadbandnumber | undefined2The deadband for auto at-setpoint detection in degrees.
animateSetpointboolean | undefinedfalseAnimate setpoint confirm transitions.
touchingboolean | undefinedfalseIndicates the user is actively adjusting (focus rendering).
headingAdvicesAngleAdvice[] | undefined[]Caution/alert arcs; triggered state derives from the current heading.
currentWindSpeedKnotsnumber | null | undefinednullThe wind speed in knots.
windFromDirectionnumber | null | undefinednullThe direction the wind is coming from in degrees.
currentSpeednumber | null | undefinednullThe current speed, number of arrows.
currentFromDirectionnumber | null | undefinednullThe direction the current is coming from in degrees.
vesselImageVesselImage | undefinedVesselImage.genericTopThe vessel silhouette centered on the compass, rotating with heading. Hidden while centerReadouts is non-empty.
centerReadoutsCompassCenterReadout[] | undefined[]Center readouts replacing the vessel: the first entry renders on top, the rest side by side below a horizontal divider. Values bind per entry source (hdg → heading, cog → courseOverGround, rot → rateOfTurnDegreesPerMinute, a dash when unset) and colors follow priorityElements. While non-empty, the crosshair's center is cut out so the readouts sit on a clean face.
hdgArrowStyleHdgArrowStyle | undefinedHdgArrowStyle.arrowHeadHDG arrow style: arrowHead (default), needle, vector, or beamLine.
cogArrowStyleCogArrowStyle | undefinedCogArrowStyle.arrowHeadCOG arrow style: arrowHead (default), needle, vector, or velocityVector.
rateOfTurnDegreesPerMinutenumber | undefined–Measured rate of turn in degrees per minute (positive = starboard, the AIS / ITU-R M.1371 convention). Drives both the bar extent and (after multiplication by rotDotAnimationFactor) the spinning dot animation.
rotDotAnimationFactornumber | undefined18Visual amplification applied only to the spinning dot animation (not to the bar extent). Default 18 keeps the legacy visual feel (≈1 rpm at 20°/min).
rotTypeRotType | undefinedRotType.dotsROT display mode: dots (spinning dots, default) or bar (arc from HDG toward COG).
rotPositionRotPosition | undefinedRotPosition.innerCircleROT track position: innerCircle (default) or scale (outer ring).
rotMaxValuenumber | undefined60Bar-extent reference value in **degrees per minute**. The bar fills the full ±rotArcExtent arc when the measured ROT equals ±rotMaxValue. Default 60 aligns with ES-TRIN 2025/1 Art. 3.02.
rotArcExtentnumber | undefined60Arc, in degrees, that the rate-of-turn bar fills when the measured rate of turn equals rotMaxValue; the bar fills ±rotArcExtent for ±rotMaxValue. Default 60. Bar mode only.
rotPortStarboardboolean | undefinedfalseColors the rate-of-turn dots or bar by the direction of the turn: starboard colors for a positive rate, port colors for a negative one. Default false.
rotAtZeroDeadbandnumber | undefinedROT_ZERO_DEADBAND_DEGSmallest rate-of-turn bar, in degrees of arc, that is drawn as a bar; a shorter one is drawn as the zero marker. Default 0.05. Bar mode only.
directionCompassDirection | undefinedCompassDirection.NorthUpOrientation mode: north-up (default), heading-up, or course-up.
stateInstrumentState | undefinedInstrumentState.activeInstrument state: active (default), loading or off. off draws the outer ring only, and loading and off draw the heading setpoint marker disabled.
priorityPriority | undefinedPriority.regularPriority.enhanced uses the blue palette for the elements in priorityElements.
priorityElementsCompassPriorityElement[] | undefined[CompassPriorityElement.hdg]Which elements the enhanced priority applies to (default: hdg).
showLabelsboolean | undefinedfalseShow compass NSEW labels. The north arrow is always shown, independent of this flag.
tickmarksInsideboolean | undefinedfalseWhen true, labels and north arrow are placed inside the outer ring.
faceDiameternumber | undefined–Outer-ring diameter in CSS pixels. When set, the instrument renders at a fixed intrinsic size derived from the ring, arc shape and label reserve — so instruments sharing the same value have identical ring circumference regardless of label width or arc extent. When unset (default), the instrument fills its container.
classNamestring | undefined–Class names added to the instrument's root element.
  • Heading: the same card without the wind, current and rate-of-turn layers.
  • Rate of turn: the turn indicator on its own.
  • Wind: the wind dial on its own.
  • Data integrity: the states a value can be in, and what the crew does.