Skip to content
design
Instruments

Speed arrows

The speed and direction of travel as up to three chevrons, with an optional readout.

On this page
1.4
Speed
KN
0.0
Speed
KN
0.0
Speed
KN
0.0
Speed
KN
Speed through water, helm
1.40
Speed
KN
Drift, cockpit repeater
Berthing alongside on a live feed: four directions colored by side and tinted when idle, beside the enhanced and regular palettes.
Code
import { ActiveColor, Direction, SpeedArrows } from "@bosunkit/instruments";
import { Sailboat } from "lucide-react";
import { useEffect, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Berthing alongside on a live feed: the speed ahead falls as the vessel stops, the bow thruster pushes her to " +
  "starboard onto the pontoon, and a burst astern stops her, with all four directions colored by side and tinted " +
  "when idle, beside the speed through water and the drift in the enhanced and regular palettes.";

const PANEL: CSSProperties = {
  display: "flex",
  flexWrap: "wrap",
  alignItems: "center",
  justifyContent: "center",
  gap: 40,
  width: "100%",
  maxWidth: 640,
};

const CROSS: CSSProperties = {
  display: "grid",
  gridTemplateColumns: "repeat(3, auto)",
  gridTemplateRows: "repeat(3, auto)",
  alignItems: "center",
  justifyItems: "center",
  gap: 12,
};

const COLUMN: CSSProperties = { display: "flex", flexDirection: "column", gap: 20 };
const LABEL: CSSProperties = { fontSize: 13, color: "var(--text-dim)" };
const VESSEL: CSSProperties = { color: "var(--text-dim)" };

/** How many chevrons a speed lights: one from 0.1 knots, two from 0.5, three from 1.0. */
function chevrons(knots: number): number {
  if (knots >= 1) {
    return 3;
  }
  if (knots >= 0.5) {
    return 2;
  }
  return knots >= 0.1 ? 1 : 0;
}

/** Longitudinal and lateral speed in knots, positive ahead and to starboard, over a forty-second approach. */
function motionAt(time: number): { ahead: number; starboard: number } {
  const phase: number = time % 40;
  const ahead: number = phase < 25 ? 1.4 * (1 - phase / 25) : -0.5 * Math.sin((Math.PI * (phase - 25)) / 15);
  const starboard: number = phase > 12 && phase < 34 ? 0.7 * Math.sin((Math.PI * (phase - 12)) / 22) : 0;
  return { ahead, starboard };
}

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 { ahead, starboard }: { ahead: number; starboard: number } = motionAt(second);
  const forward: number = Math.max(0, ahead);
  const astern: number = Math.max(0, -ahead);
  const toStarboard: number = Math.max(0, starboard);
  const toPort: number = Math.max(0, -starboard);

  return (
    <div style={PANEL}>
      <div style={CROSS}>
        <span />
        <SpeedArrows
          direction={Direction.forward}
          speedKnots={forward}
          nActiveArrows={chevrons(forward)}
          activeColor={ActiveColor.Direction}
          tintedArrows
          readout
          fractionDigits={1}
          maxDigits={1}
        />
        <span />
        <SpeedArrows
          direction={Direction.left}
          speedKnots={toPort}
          nActiveArrows={chevrons(toPort)}
          activeColor={ActiveColor.Direction}
          tintedArrows
          readout
          fractionDigits={1}
          maxDigits={1}
        />
        <Sailboat size={40} style={VESSEL} aria-hidden />
        <SpeedArrows
          direction={Direction.right}
          speedKnots={toStarboard}
          nActiveArrows={chevrons(toStarboard)}
          activeColor={ActiveColor.Direction}
          tintedArrows
          readout
          fractionDigits={1}
          maxDigits={1}
        />
        <span />
        <SpeedArrows
          direction={Direction.backward}
          speedKnots={astern}
          nActiveArrows={chevrons(astern)}
          activeColor={ActiveColor.Direction}
          tintedArrows
          readout
          fractionDigits={1}
          maxDigits={1}
        />
        <span />
      </div>

      <div style={COLUMN}>
        <div>
          <div style={LABEL}>Speed through water, helm</div>
          <SpeedArrows
            direction={ahead >= 0 ? Direction.forward : Direction.backward}
            speedKnots={Math.abs(ahead)}
            nActiveArrows={chevrons(Math.abs(ahead))}
            activeColor={ActiveColor.Enhanced}
            tintedArrows
            readout
            fractionDigits={2}
            maxDigits={2}
          />
        </div>
        <div>
          <div style={LABEL}>Drift, cockpit repeater</div>
          <SpeedArrows
            direction={starboard >= 0 ? Direction.right : Direction.left}
            speedKnots={Math.abs(starboard)}
            nActiveArrows={chevrons(Math.abs(starboard))}
            activeColor={ActiveColor.Regular}
          />
        </div>
      </div>
    </div>
  );
}

Speed arrows show the speed as up to three stacked chevrons that point along the direction of travel, with an optional readout above them that prints the speed in knots. activeColor can color the chevrons by the direction of travel, green for forward and right and red for backward and left. It ports obc-speed-arrows; the overview lists the other instruments.

Examples

Speed arrows

6
Speed
KN
A speed of 6.4 knots as a readout above the three chevrons.
Code
import { SpeedArrows } from "@bosunkit/instruments";
import type { ReactElement } from "react";

export const description: string = "A speed of 6.4 knots as a readout above the three chevrons.";

export default function SpeedArrowsExample(): ReactElement {
  return (
    <div style={{ width: 200, height: 200 }}>
      <SpeedArrows speedKnots={6.4} readout />
    </div>
  );
}

Direction, magnitude and thresholds

speedKnots, direction and nActiveArrows are separate inputs. The caller chooses the chevron thresholds; three arrows do not define a universal speed. Keep those thresholds stable and explain them when the count informs a decision.

Label whether the motion is longitudinal, transverse, over ground or through water. Direction colors describe travel direction, not alert priority. Keep the numeric readout or an adjacent labeled reading when an exact speed matters, and show an unavailable state outside the arrows when the source is lost.

Props

PropTypeDefaultDescription
speedKnotsnumber | undefined0Speed shown in the readout (knots).
readoutboolean | undefinedfalseShow the numeric speed readout above the arrows.
maxDigitsnumber | undefined2Maximum digits reserved by the readout.
fractionDigitsnumber | undefined0Fraction digits shown in the readout.
nActiveArrowsnumber | undefined0How many of the three chevrons are active (0–3).
directionDirection | undefinedDirection.forwardTravel direction the chevrons point along.
activeColorActiveColor | undefinedActiveColor.RegularActive chevron palette: Regular (neutral), Enhanced (blue), or Direction — starboard green for forward/right, port red for backward/left (the IMO port/starboard convention).
tintedArrowsboolean | undefinedfalseRender inactive chevrons tinted instead of omitting them.
classNamestring | undefined–Class names added to the instrument's root element.