Skip to content
design
Components

Header bar

The bar atop every screen, holding the logo, the page title, the alert zone and the status strip.

On this page
The anchor page's header bar at three widths, with the anchor drag alarm in the alert zone. Choose a width; press ACK and the zone clears when the board confirms.
Code
import { AlertZone, type AlertZoneAlert } from "@bosunkit/components/alert-zone";
import { HeaderBar } from "@bosunkit/components/header-bar";
import { Label } from "@bosunkit/components/label";
import { SegmentedRadioGroup, type SegmentedRadioOption } from "@bosunkit/components/segmented-radio-group";
import { StatusStrip, type SensorSummary } from "@bosunkit/components/status-strip";
import { Switch } from "@bosunkit/components/switch";
import { useId, useState, type CSSProperties, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "The header bar on the anchor page at a desktop's, a tablet's and a phone's width: the logo, the page title, the " +
  "anchor drag alarm in the alert zone with ACK, the status strip and a night switch; on a phone the alert zone " +
  "takes a row of its own.";

export const readsClock: boolean = true;

const WIDTHS: readonly SegmentedRadioOption[] = [
  { value: "70em", label: "Desktop" },
  { value: "48em", label: "Tablet" },
  { value: "390px", label: "Phone" },
];

const SENSORS: readonly SensorSummary[] = [
  { name: "gps", integrity: "fresh", ageSeconds: 1 },
  { name: "depth", integrity: "fresh", ageSeconds: 1 },
  { name: "wind", integrity: "stale", ageSeconds: 48 },
];

/**
 * The bar at the chosen width in a frame that scrolls sideways. On a declared screen (a device preview, or the hero
 * narrowed) the screen is the width: the switch hides and the bar takes the screen's width.
 */
const FRAME_STYLES: string = `
  .header-bar-showcase__frame {
    width: 100%;
    overflow-x: auto;
    border: 1px solid var(--border);
  }
  .header-bar-showcase__screen {
    container: bk-screen / inline-size;
    width: var(--header-bar-showcase-width);
  }
  @container bk-screen (min-width: 0px) {
    .header-bar-showcase__width {
      display: none;
    }
    .header-bar-showcase__screen {
      width: auto;
    }
  }
`;

function ignore(): void {
  // The example changes nothing on these taps.
}

export default function Showcase(): ReactElement {
  const nightId: string = useId();
  const [width, setWidth]: [string, Dispatch<SetStateAction<string>>] = useState<string>("70em");
  const [night, setNight]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);
  const [openedAt]: [number, Dispatch<SetStateAction<number>>] = useState<number>((): number => Date.now());
  const [acknowledged, setAcknowledged]: [boolean, Dispatch<SetStateAction<boolean>>] = useState<boolean>(false);

  function acknowledge(): Promise<void> {
    // Stands in for the board: the zone clears once the next snapshot confirms the acknowledgement.
    return new Promise<void>((resolve: () => void): void => {
      setTimeout((): void => {
        setAcknowledged(true);
        resolve();
      }, 1200);
    });
  }

  const alarm: AlertZoneAlert | null = acknowledged
    ? null
    : {
        id: "drag",
        title: "Anchor dragging, 41 m from the drop point",
        priority: "alarm",
        raisedAt: openedAt - 3 * 60_000,
      };

  return (
    <div style={{ display: "grid", gap: "var(--space-md)", width: "100%" }}>
      <style href="bosunkit-example-header-bar" precedence="default">
        {FRAME_STYLES}
      </style>
      <div className="header-bar-showcase__width">
        <SegmentedRadioGroup label="Bar width" options={WIDTHS} value={width} onValueChange={setWidth} />
      </div>
      <div className="header-bar-showcase__frame">
        <div className="header-bar-showcase__screen" style={{ "--header-bar-showcase-width": width } as CSSProperties}>
          <HeaderBar
            title="Anchor"
            alertZone={
              <AlertZone
                count={acknowledged ? 0 : 1}
                alert={alarm}
                acknowledgeAction={acknowledge}
                boardHref="#alerts"
              />
            }
            statusStrip={
              <StatusStrip
                connection="live"
                lastEventAt={openedAt}
                activity="anchored"
                sensors={SENSORS}
                risk={null}
                doNotDisturb={{ enabled: false, missed: 0, onToggle: ignore }}
                nightSuggested={false}
                onDismissSuggestion={ignore}
                unacknowledgedAlertIds={acknowledged ? [] : ["drag"]}
              />
            }
            actions={
              <>
                <Label htmlFor={nightId}>Night</Label>
                <Switch id={nightId} checked={night} onCheckedChange={setNight} />
              </>
            }
          />
        </div>
      </div>
    </div>
  );
}

The header bar heads every screen of the app shell. It holds, in order, the logo, the page's title, the alert zone, the status strip and any controls the screen adds after the strip. It reads no data: the caller passes its alert zone and status strip with their props. The title is the page's h1; on a page that heads itself with its own h1, titleHeading={false} draws the title for sight and leaves the heading to the page.

The alert zone takes the room between the title and the strip, centered, and never less than 14em of it: when the bar is too narrow for that and the strip, the strip wraps before the zone shrinks. The bar measures its own width:

Bar widthWhat changes
52.5em and widerOne row: logo, title, alert zone, status strip, controls.
44em to 52.5emThe title gives way, kept for a screen reader when it is the h1.
under 44emOne compact row of the logo, without its tagline, and the status strip packed at the end; the alert zone takes a row of its own under them, so the alert's name has room and ACK lies under no other control.
under 30emThe strip gives up the vessel's activity before it would wrap; the connection, the sensors, do not disturb, brightness and the clock stay.
under 24emThe logo keeps its mark and gives up its name.

Inside the app shell the bar is the page's banner landmark and stays on the alert layer, so a dialog or the dimmer never covers the alert zone. The status strip and the alert zone in it take their narrow forms by the bk-screen container, which the app shell declares; the hero declares it on its frame.

01The alert zone never gives wayCaptions and the title go first when room runs out; the alert zone keeps its name, age and ACK at every width.
02One order everywhereLogo, title, alert zone, status strip: the crew find each part in the same place on every page and every device.
03Props in, nothing fetchedThe bar lays out what it is given. The alert zone and the status strip take their values from the app, through their own props.

Examples

Nothing to acknowledge

Tides

With nothing unacknowledged the alert zone is empty and keeps its place, so an arriving alert appears there without moving the rest of the bar.
Code
import { AlertZone } from "@bosunkit/components/alert-zone";
import { HeaderBar } from "@bosunkit/components/header-bar";
import type { ReactElement } from "react";

export const description: string =
  "With nothing unacknowledged the alert zone is empty and keeps its place, so an arriving alert appears there " +
  "without moving the rest of the bar.";

async function acknowledge(): Promise<void> {
  // The example acknowledges nothing.
}

export default function NoAlert(): ReactElement {
  return (
    <div style={{ width: "100%" }}>
      <HeaderBar
        title="Tides"
        alertZone={<AlertZone count={0} alert={null} acknowledgeAction={acknowledge} boardHref="#alerts" />}
      />
    </div>
  );
}

A page with its own heading

On a page that heads itself with a page header, titleHeading={false} draws the bar's title for sight only, so the page has one h1.
Code
import { HeaderBar } from "@bosunkit/components/header-bar";
import { PageHeader } from "@bosunkit/components/page-header";
import type { ReactElement } from "react";

export const description: string =
  "On a page that heads itself with a page header, titleHeading={false} draws the bar's title for sight only, so " +
  "the page has one h1.";

export default function PageWithItsOwnHeading(): ReactElement {
  return (
    <div style={{ display: "grid", gap: "var(--space-md)", width: "100%" }}>
      <HeaderBar title="Log" titleHeading={false} />
      <PageHeader eyebrow="Passage" title="Log" subtitle="Camaret to Audierne, 14 entries" />
    </div>
  );
}

On a phone

At a phone's width the title gives way, kept for a screen reader, and the alert zone takes a row of its own under the logo, where a long name ends in an ellipsis and ACK stays whole.
Code
import { AlertZone } from "@bosunkit/components/alert-zone";
import { HeaderBar } from "@bosunkit/components/header-bar";
import { useState, type Dispatch, type ReactElement, type SetStateAction } from "react";

export const description: string =
  "At a phone's width the title gives way, kept for a screen reader, and the alert zone takes a row of its own " +
  "under the logo, where a long name ends in an ellipsis and ACK stays whole.";

export const readsClock: boolean = true;

async function acknowledge(): Promise<void> {
  // The example acknowledges nothing.
}

export default function OnAPhone(): ReactElement {
  const [raisedAt]: [number, Dispatch<SetStateAction<number>>] = useState<number>((): number => Date.now() - 6 * 60_000);
  return (
    <div style={{ width: "390px", maxWidth: "100%", border: "1px solid var(--border)" }}>
      <HeaderBar
        title="Situation"
        alertZone={
          <AlertZone
            count={2}
            alert={{ id: "bilge", title: "Bilge pump running for 4 minutes", priority: "warning", raisedAt }}
            acknowledgeAction={acknowledge}
            boardHref="#alerts"
          />
        }
      />
    </div>
  );
}

Props

PropTypeDefaultDescription
logoReactNode | undefined–The mark at the start of the bar; the small bosun logo when unset, nothing with null.
titlestring | undefined–The page's name, beside the logo.
titleHeadingboolean | undefinedtrueWhether the title is the page's h1: on by default. Set it to false on a page that heads itself with its own h1 (a PageHeader): the title is then drawn for sight and hidden from assistive technology, which reads the page's own heading.
alertZoneReactNode | undefined–The alert zone (AlertZone). It takes the room left between the title and the status strip, never less than 14em; when the bar is too narrow for that, it takes a row of its own under the logo and the strip.
statusStripReactNode | undefined–The status strip (StatusStrip), at the end of the bar.
actionsReactNode | undefined–Controls after the status strip, such as a screen's night switch.
classNamestring | undefined–Classes added to the root's own.
testIdstring | undefined–Becomes data-testid on the root; the title is ${testId}--title, the alert zone's slot ${testId}--alert-zone.
refRef<HTMLDivElement> | undefined–The root element.
Do

Chart

Alert zone
Give the bar the alert zone, so the worst unacknowledged alert heads every page.
Don't

Chart

Zoom · Layers · Route · Measure · Share
Don't fill the bar with page controls. They push the status strip onto a second row and crowd the alert zone.
  • App shell: the frame whose header slot the bar fills.
  • Alert zone: the worst unacknowledged alert, its age and ACK.
  • Status strip: the stream, the vessel, the sensors and the clock.