Skip to content
design
Foundations

Motion

Short, functional transitions. Attention follows the alert component’s lifecycle; measured values update immediately.

On this page
  • Anchor drag38 m outside the swing circle.AlarmAnchor watchUnacknowledged21:02 UTC
  • Bilge pump running6 min without stopping.AlarmBilgeUnacknowledged21:03 UTC
  • Shallow water2.1 m under the keel.AlarmDepthAcknowledged20:58 UTC
DepthSOUNDER
8.4m
▲ +0.3 m / 10 min
True windMAST
14G22kn
TWD 214°
SOGGPS
0.4kn
COG 087°
Two unacknowledged alarms flash in phase, on one clock. The acknowledged alarm is steady, and nothing else on screen moves.
01Motion explains a changeA transition shows where something came from or went, such as a panel opening or a row becoming active. It never decorates.
02Attention belongs to alertsUse the alert component’s state and shared blink clock. Status markers and decorative content do not acquire a repeating attention animation.
03Reduced motion keeps no movementUnder prefers-reduced-motion nothing moves or changes size: an element that would rise or scale only fades. An unacknowledged alarm's flash continues, because it is the signal that someone has to act.
04Alarms never wait for an animationAn alert's text is in the page and announced the moment it arrives. Its entrance only fades and slides, so nothing the crew needs waits for the motion to finish.

Durations

Three durations cover every transition. They are short so that the interface keeps up with a hand on a tablet in a seaway: a state change finishes before the next touch.

TokenValuePreview
--motion-fast-duration120ms
--motion-medium-duration200ms
--motion-slow-duration320ms
--motion-standard-easingcubic-bezier(0.2, 0, 0, 1)—
--motion-enter-easingcubic-bezier(0, 0, 0, 1)—
--motion-exit-easingcubic-bezier(0.3, 0, 1, 1)—
--motion-rise-distance4px
--motion-scale-from0.96—

The previews run six times slower than the token, so the difference between the three is visible.

DurationUse
--motion-fast-durationHover and press feedback, a chevron opening a group, a change of value, the exit of what enters on medium
--motion-medium-durationA dialog, menu, popover, tooltip, toast or small element entering, a toast leaving, an item or indicator sliding to its new place
--motion-slow-durationA value's highlight, a cleared alert fading back, a progress bar filling

An exit takes a shorter duration than the entrance it undoes: the crew asked for the thing to go, so it goes quickly. A toast is the exception: it slides out past the screen's edge over the medium duration. No transition lasts longer than the slow duration; a saved chip stays a while and then fades over the slow duration. The repeating motions are the alarm flash and the progress indicators (the spinner, a skeleton's shimmer, a saving chip's spinner), and the progress indicators stop under reduced motion.

Hover, press, focus and selection feedback may change a control's color, background, border and shadow over the fast duration on the standard curve. It moves nothing and lays nothing out again, so it is the one change of something other than position, size and opacity that transitions. Every transition and animation takes its duration and its curve from the motion tokens; a turning spinner's curve is linear.

The shared flash clock

Unacknowledged alerts flash on one clock, in one of three tempos (--bk-flash-*): fast (400 ms on, 400 ms off), slow (1200 ms on, 400 ms off) and very slow (2800 ms on, 400 ms off). The on phase carries the tempo, so a flashing alert stays coloured for most of its cycle; every tempo goes off for the same 400 ms, and the cycles nest, so all tempos light up together at the start of the slowest one. Every flashing element anchors the same animation to the document timeline, so alerts in different places pulse in phase. Reduced motion does not remove the flash: it is the signal that someone has to act.

An unacknowledged alarm or emergency flashes on the fast tempo. The slow and very slow tempos belong to the vendored instruments: OpenBridge's alert frame flashes a rectified alarm very slowly, and draws a warning steady.

Easing

Three curves, one for each direction of travel. Something moving within the screen uses --motion-standard-easing, which starts quickly and settles slowly. Something arriving uses --motion-enter-easing and decelerates into place; something leaving uses --motion-exit-easing and accelerates away. Components that animate from JavaScript read the same values from @bosunkit/tokens/motion.

CurveCSS tokenControl points
Standard--motion-standard-easing0.2, 0, 0, 1
Enter--motion-enter-easing0, 0, 0, 1
Exit--motion-exit-easing0.3, 0, 1, 1
TIMEPROGRESS
--motion-standard-easing · cubic-bezier(0.2, 0, 0, 1)

The motion prop

Components that expose motion use a shared type and vocabulary. Check the component’s props table: this is not a universal prop on every kit component. CSS-only transitions and instrument blink states have their own contract:

ValueWhat it does
trueThe component's own motion
falseNo motion
"fade", "rise" or "scale"Enters and leaves with that preset
{ enter, exit, change, attention }Each kind named on its own; a kind left out does not move
  • enter plays when the element mounts, over 200 ms on the enter curve.
  • exit plays when the element is removed inside a MotionPresence from @bosunkit/components/motion, over 120 ms on the exit curve. The element is inert from the start of its exit: it takes no press or focus and leaves the accessibility tree, so a second tap on a leaving row cannot act on the row that takes its place.
  • change plays when the element's value changes: "pop" grows it briefly, "fade" dips its opacity, "highlight" tints its background. The new value is on screen from the first frame of the change. A measured value takes "highlight" only. A pop and a highlight play at most once every 2 s per element, so a count that climbs quickly does not keep moving.
  • attention takes an alert's priority and whether it is acknowledged, and flashes the element on the fast tempo while an alarm or an emergency is unacknowledged; no other priority flashes. The caller owns the acknowledged state, and the flash is never the only signal: the word and the priority color stay. It belongs to MotionElement: a component's motion prop drops it, since only an alert component flashes, and the badge and the alert card set their own flash.

The following components have motion defaults. motion={false} disables optional motion where the prop is provided; it is not a way to suppress a separate alert instrument’s lifecycle:

ComponentDefault
Dialog, select list, combo box list, collapsible content, tooltipOpen and close (in their stylesheets)
Popover, menu, context menu, action menu, toastOpen and close (in their stylesheets)
Card grid with alert cards and section dividersEnter, exit and reorder
Alert zoneIts alert rises in and fades out; an unacknowledged alarm or emergency flashes on the alarm clock, with or without motion
AlertList with AlertListRowImmediate ordering; no list animation
TabsThe indicator slides between tabs
BadgePops on a change; a red badge flashes while the caller marks it unacknowledged, with or without motion
Segmented radio groupThe mark slides between options
Chess boardA move slides
Button, due chipA changed label fades in
Chat bubble, skeleton, form field error, setting chips, connection notices, reading, instrument pillEnter and exit
Empty state, event timeline entry, copy button glyphEnter and exit

A default entrance or exit plays where the caller adds and removes the element: inside a MotionPresence, or a component's own list (the form field's error, the setting chips' entries, the card grid). Nothing animates in on a page's first render. A reading's or a pill's highlight stays opt-in, since a tint on every update of a fast instrument would flicker. The tooltip has no prop.

fadeGPS 3D
riseGPS 3D
scaleGPS 3D
The three presets on one badge each: fade changes opacity only, rise moves up a few pixels as it fades in, scale grows from slightly smaller. Each enters over the medium duration and leaves over the fast one.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { MotionPresence, type MotionPreset } from "@bosunkit/components/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The three presets on one badge each: fade changes opacity only, rise moves up a few pixels as it fades in, " +
  "scale grows from slightly smaller. Each enters over the medium duration and leaves over the fast one.";

const PRESETS: readonly MotionPreset[] = ["fade", "rise", "scale"];

export default function Presets(): ReactElement {
  const [shown, setShown]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(true);
  return (
    <div style={{ display: "flex", flexDirection: "column", alignItems: "flex-start", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setShown((current: boolean): boolean => !current);
        }}
      >
        {shown ? "Hide the badges" : "Show the badges"}
      </Button>
      <div style={{ display: "flex", gap: "var(--space-md)", minHeight: 24 }}>
        {PRESETS.map(
          (preset: MotionPreset): ReactElement => (
            <div key={preset} style={{ display: "flex", flexDirection: "column", gap: 6, width: 96 }}>
              <code style={{ fontSize: 11, color: "var(--text-dim)" }}>{preset}</code>
              <MotionPresence>
                {shown && (
                  <Badge key={preset} variant="green" motion={preset}>
                    GPS 3D
                  </Badge>
                )}
              </MotionPresence>
            </div>
          ),
        )}
      </div>
    </div>
  );
}

Each kind of motion on its own, played by a button, with the tokens it runs on. The kit reads reduced motion from the operating system only, so a page cannot switch it for one example: turn on reduced motion in the system settings to see each one's reduced form. Entrances and exits then only fade, a pop and a highlight do not play, a reorder and the tabs indicator move at once, and the alarm flash keeps flashing.

Enter and exit

enter 200 ms --motion-enter-easing · exit 120 ms --motion-exit-easing
Enter and exit: a badge the caller adds and removes inside a MotionPresence fades in over the medium duration on the enter curve, and out over the fast duration on the exit curve.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { MotionPresence } from "@bosunkit/components/motion";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Enter and exit: a badge the caller adds and removes inside a MotionPresence fades in over the medium duration " +
  "on the enter curve, and out over the fast duration on the exit curve.";

export default function EnterExit(): ReactElement {
  const [shown, setShown]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setShown((current: boolean): boolean => !current);
        }}
      >
        {shown ? "Play the exit" : "Play the entrance"}
      </Button>
      <span style={{ minWidth: 80 }}>
        <MotionPresence initial={false}>
          {shown && (
            <Badge key="fix" variant="green">
              GPS 3D
            </Badge>
          )}
        </MotionPresence>
      </span>
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`enter ${String(MOTION_DURATIONS.medium * 1000)} ms --motion-enter-easing · exit ${String(
          MOTION_DURATIONS.fast * 1000,
        )} ms --motion-exit-easing`}
      </code>
    </div>
  );
}

Change: pop

3 AIS targets120 ms --motion-standard-easing
Change, pop: a badge's count grows briefly and settles when it changes, over the fast duration on the standard curve. The new count is there from the first frame.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Change, pop: a badge's count grows briefly and settles when it changes, over the fast duration on the standard " +
  "curve. The new count is there from the first frame.";

export default function ChangePop(): ReactElement {
  const [count, setCount]: [number, Dispatch<SetStateAction<number>>] = useState<number>(3);
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setCount((current: number): number => current + 1);
        }}
      >
        Another target
      </Button>
      <Badge variant="blue">{`${String(count)} AIS targets`}</Badge>
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`${String(MOTION_DURATIONS.fast * 1000)} ms --motion-standard-easing`}
      </code>
    </div>
  );
}

Change: fade

120 ms --motion-standard-easing
Change, fade: a button's new label fades in when it changes, over the fast duration on the standard curve. The button keeps its focus, and the fade stays under reduced motion, as it changes only opacity.
Code
import { Button } from "@bosunkit/components/button";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Change, fade: a button's new label fades in when it changes, over the fast duration on the standard curve. The " +
  "button keeps its focus, and the fade stays under reduced motion, as it changes only opacity.";

export default function ChangeFade(): ReactElement {
  const [acknowledged, setAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        variant="primary"
        onClick={(): void => {
          setAcknowledged((current: boolean): boolean => !current);
        }}
      >
        {acknowledged ? "Acknowledged" : "Acknowledge"}
      </Button>
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`${String(MOTION_DURATIONS.fast * 1000)} ms --motion-standard-easing`}
      </code>
    </div>
  );
}

Highlight

320 ms --motion-exit-easing · at most every 2 s
Highlight, opt-in: a reading tints its background when a new value arrives, over the slow duration on the exit curve, at most once per interval. Press quickly and only the first press in each interval tints.
Code
import { Button } from "@bosunkit/components/button";
import { Reading } from "@bosunkit/components/reading";
import { MOTION_DURATIONS, MOTION_HIGHLIGHT_INTERVAL } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Highlight, opt-in: a reading tints its background when a new value arrives, over the slow duration on the exit " +
  "curve, at most once per interval. Press quickly and only the first press in each interval tints.";

const DEPTHS: readonly number[] = [8.4, 8.1, 7.6, 7.2, 7.5];

export default function Highlight(): ReactElement {
  const [sample, setSample]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setSample((current: number): number => (current + 1) % DEPTHS.length);
        }}
      >
        Next depth reading
      </Button>
      <Reading
        value={DEPTHS[sample]}
        unit="m"
        label="Depth"
        source="DST810"
        fractionDigits={1}
        motion={{ change: "highlight" }}
      />
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`${String(MOTION_DURATIONS.slow * 1000)} ms --motion-exit-easing · at most every ${String(
          MOTION_HIGHLIGHT_INTERVAL,
        )} s`}
      </code>
    </div>
  );
}

Attention

Bilge alarm--bk-flash-fast-on · --bk-flash-fast-off
Attention: an unacknowledged alarm's dot flashes on the shared alert clock until it is acknowledged, the only loop on screen. It keeps flashing under reduced motion.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Attention: an unacknowledged alarm's dot flashes on the shared alert clock until it is acknowledged, the only loop " +
  "on screen. It keeps flashing under reduced motion.";

export default function Attention(): ReactElement {
  const [acknowledged, setAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setAcknowledged((current: boolean): boolean => !current);
        }}
      >
        {acknowledged ? "Raise again" : "Acknowledge"}
      </Button>
      <Badge variant="red" unacknowledged={!acknowledged}>
        {acknowledged ? "Bilge alarm, acknowledged" : "Bilge alarm"}
      </Badge>
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>--bk-flash-fast-on · --bk-flash-fast-off</code>
    </div>
  );
}

Reorder

200 ms --motion-standard-easing
Wind above reef threshold
Shallow water
House battery low
Reorder: an alert list slides its cards to a new order over the medium duration on the standard curve. While a card has focus or the pointer is pressed on it, the new order applies at once.
Code
import { AlertCard } from "@bosunkit/components/alert-card";
import { Button } from "@bosunkit/components/button";
import { AlertCardGrid } from "@bosunkit/components/alert-card-grid";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Reorder: an alert list slides its cards to a new order over the medium duration on the standard curve. While a " +
  "card has focus or the pointer is pressed on it, the new order applies at once.";

interface Item {
  readonly id: string;
  readonly title: string;
}

const ITEMS: readonly Item[] = [
  { id: "wind", title: "Wind above reef threshold" },
  { id: "depth", title: "Shallow water" },
  { id: "battery", title: "House battery low" },
];

export default function Reorder(): ReactElement {
  const [order, setOrder]: [readonly Item[], Dispatch<SetStateAction<readonly Item[]>>] =
    useState<readonly Item[]>(ITEMS);
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)", width: "100%" }}>
      <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
        <Button
          size="sm"
          onClick={(): void => {
            setOrder((current: readonly Item[]): Item[] => [...current.slice(1), ...current.slice(0, 1)]);
          }}
        >
          Move the top card down
        </Button>
        <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
          {`${String(MOTION_DURATIONS.medium * 1000)} ms --motion-standard-easing`}
        </code>
      </div>
      <AlertCardGrid>
        {order.map(
          (item: Item): ReactElement => (
            <AlertCard key={item.id} className="alarm--full" priority="warning" title={item.title} />
          ),
        )}
      </AlertCardGrid>
    </div>
  );
}

Tabs indicator

200 ms --motion-standard-easing
Tabs indicator: the underline slides to the tab the crew selects, over the medium duration on the standard curve.
Code
import { Tabs, type TabsOption } from "@bosunkit/components/tabs";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Tabs indicator: the underline slides to the tab the crew selects, over the medium duration on the standard curve.";

const TABS: readonly TabsOption[] = [
  { id: "passage", label: "Passage" },
  { id: "weather", label: "Weather" },
  { id: "tides", label: "Tides" },
];

export default function TabsIndicator(): ReactElement {
  const [active, setActive]: [string, Dispatch<SetStateAction<string>>] = useState<string>("passage");
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-sm)" }}>
      <Tabs tabs={TABS} value={active} onValueChange={setActive} />
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`${String(MOTION_DURATIONS.medium * 1000)} ms --motion-standard-easing`}
      </code>
    </div>
  );
}

Overlay open and close

open 200 ms --motion-enter-easing · close 120 ms --motion-exit-easing
Overlay open and close: a dialog scales in over a fading overlay, in its stylesheet on the same tokens, and leaves faster than it came. Its title and body are there from the first frame.
Code
import { Button } from "@bosunkit/components/button";
import { TaskDialog } from "@bosunkit/components/task-dialog";
import { MOTION_DURATIONS } from "@bosunkit/tokens/motion";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Overlay open and close: a dialog scales in over a fading overlay, in its stylesheet on the same tokens, and leaves " +
  "faster than it came. Its title and body are there from the first frame.";

export default function Overlay(): ReactElement {
  const [open, setOpen]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  function close(): void {
    setOpen(false);
  }
  return (
    <div style={{ display: "flex", flexWrap: "wrap", alignItems: "center", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setOpen(true);
        }}
      >
        Open the dialog
      </Button>
      <code style={{ fontSize: 11, color: "var(--text-dim)" }}>
        {`open ${String(MOTION_DURATIONS.medium * 1000)} ms --motion-enter-easing · close ${String(
          MOTION_DURATIONS.fast * 1000,
        )} ms --motion-exit-easing`}
      </code>
      <TaskDialog
        open={open}
        onOpenChange={(open): void => { if (!open) (close)(); }}
        title="Engine hours logged"
        actions={
          <Button variant="primary" onClick={close}>
            Close
          </Button>
        }
      >
        1,842 engine hours are in the log. The next oil change is due at 1,900.
      </TaskDialog>
    </div>
  );
}

Motion on and off

motion on (default)3 AIS targetsDue tomorrow
motion={false}3 AIS targetsDue tomorrow
Motion on and motion={false} side by side: one press changes both columns, and only the left one moves.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { DueChip } from "@bosunkit/components/due-chip";
import { useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "Motion on and motion={false} side by side: one press changes both columns, and only the left one moves.";

const COLUMN: CSSProperties = { display: "flex", flexDirection: "column", alignItems: "flex-start", gap: 10, width: 220 };

export default function OnOff(): ReactElement {
  const [step, setStep]: [number, Dispatch<SetStateAction<number>>] = useState<number>(0);
  const overdue: boolean = step % 2 === 1;
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-md)" }}>
      <Button
        size="sm"
        onClick={(): void => {
          setStep((current: number): number => current + 1);
        }}
      >
        Change everything
      </Button>
      <div style={{ display: "flex", flexWrap: "wrap", gap: "var(--space-lg)" }}>
        <div style={COLUMN}>
          <code style={{ fontSize: 11, color: "var(--text-dim)" }}>motion on (default)</code>
          <Badge variant="blue">{`${String(step + 3)} AIS targets`}</Badge>
          <DueChip priority={overdue ? "warning" : "caution"} label={overdue ? "Overdue" : "Due tomorrow"} />
          <Button size="sm">{overdue ? "Acknowledged" : "Acknowledge"}</Button>
        </div>
        <div style={COLUMN}>
          <code style={{ fontSize: 11, color: "var(--text-dim)" }}>motion={"{false}"}</code>
          <Badge variant="blue" motion={false}>{`${String(step + 3)} AIS targets`}</Badge>
          <DueChip
            priority={overdue ? "warning" : "caution"}
            label={overdue ? "Overdue" : "Due tomorrow"}
            motion={false}
          />
          <Button size="sm" motion={false}>
            {overdue ? "Acknowledged" : "Acknowledge"}
          </Button>
        </div>
      </div>
    </div>
  );
}

Alerts and live values

AlertCardGrid can animate alert cards entering, leaving and changing order. The dedicated AlertList applies its order immediately and has no motion prop. For compositions that animate, these limits apply:

  • An alert arrives at once. Its text is in the page and announced by its live region the moment it arrives. The entrance changes only opacity and transform, lasts at most 200 ms, and never delays the text or the announcement. Under reduced motion it only fades.
  • Removal follows the alert policy. When the caller removes an alert, its exit is fast and inert. Acknowledgment alone does not mean the cause has cleared or that the alert should leave the list.
  • The list moves to its new order, unless the crew is using it. Items slide to their new places. While an item has pointer or keyboard focus, or the pointer is pressed on one, the new order applies at once, so nothing moves under a finger. Under reduced motion the new order always applies at once.
  • A change of priority and an acknowledgment change the card at once. Only an unacknowledged alarm or emergency flashes.
  • ACK is pending until bosund answers. The alert keeps flashing until the acknowledgment is confirmed, and a failed acknowledgment offers ACK again.

A measured value never rolls or tweens: it jumps to the latest measurement. A reading or an instrument pill may highlight its update when the caller asks, with motion={{ change: "highlight" }}: its background tints for 320 ms, at most once every 2 s per value, so a fast instrument does not flicker. Under reduced motion it never highlights.

The alarm flash

An unacknowledged alarm or emergency flashes on the fast tempo: 400 ms on, 400 ms off. Every flashing element on screen runs on the same clock, anchored to the start of the page, so two alarms never flash out of step. The flash stops when the acknowledgment is confirmed; the alert then stays red and steady until it clears.

  • Anchor drag38 m outside the swing circle.AlarmAnchor watchUnacknowledged21:02 UTC
  • Bilge pump running6 min without stopping.AlarmBilgeUnacknowledged21:03 UTC
Unacknowledged, in phase
  • Shallow water2.1 m under the keel.AlarmDepthAcknowledged20:58 UTC
Acknowledged: steady

A warning and a caution are steady, acknowledged or not: AlertListRow gives its AlertIcon steady for every priority that does not flash. The timing tokens are --bk-flash-fast-on and --bk-flash-fast-off for the alarm, and the slow and very slow pairs the vendored instruments use. The rows on this page are the kit's alert list row on that clock. These implementation timings are not a certification claim.

Using motion

Do
GPS 3D
Keep a live marker steady. The dot says the fix is current; it doesn't need to move to say so.
Don't

GPS 3D

Keep status markers steady. A repeating pulse borrows the attention signal that belongs to an alert.
Do

8.4m

Update a live value in place, at once. The number on screen is always the last one measured.
Don't
8.18.4m
Don't animate a value from old to new. While it rolls, the screen shows depths that were never measured.

Reduced motion

When the device asks for reduced motion, nothing on screen moves or changes size. An element that would rise or scale only fades, over the same duration, so the crew sees that something arrived; a change's pop and a value's highlight do not play, and a list's new order applies at once. The flash of an unacknowledged alarm continues: reduced motion removes motion that is not essential, and the flash is how an alarm says that someone has to act (MSC.302 §7.3.3). Acknowledging the alarm stops the flash, as it does without the preference.

A page can choose for the device with data-prefers-reduced-motion on its root element: reduce applies the reduced motion whatever the device asks, no-preference the full motion, and with neither the device decides. Every kit stylesheet and usePrefersReducedMotion read it, so an application's motion setting, or this site's motion switch, reaches every component.

Documents, diagrams and reduced motion

An exported chart or architecture diagram must communicate without animation. Make the result and the final state readable in a static frame. A replay in a report needs an explicit replay label and a fixed source period; an animated specimen is not evidence of a live connection.

Check reduced motion on the actual component used. Kit instrument alert clocks preserve essential alert-state flashing, while optional movement and scaling follow reduced-motion preferences. Application adapters can have different behavior; a foundation example does not establish that every consumer has adopted the kit contract.

The public site

The public site may give its own pages entrance motion, on the same tokens and under the same reduced-motion rule. The app's rules hold for every screen the crew uses.