Header bar
The bar atop every screen, holding the logo, the page title, the alert zone and the status strip.
On this page
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 width | What changes |
|---|---|
| 52.5em and wider | One row: logo, title, alert zone, status strip, controls. |
| 44em to 52.5em | The title gives way, kept for a screen reader when it is the h1. |
| under 44em | One 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 30em | The strip gives up the vessel's activity before it would wrap; the connection, the sensors, do not disturb, brightness and the clock stay. |
| under 24em | The 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.
Examples
Nothing to acknowledge
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
LogCamaret to Audierne, 14 entries
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
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
| Prop | Type | Default | Description |
|---|---|---|---|
| logo | ReactNode | undefined | – | The mark at the start of the bar; the small bosun logo when unset, nothing with null. |
| title | string | undefined | – | The page's name, beside the logo. |
| titleHeading | boolean | undefined | true | Whether 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. |
| alertZone | ReactNode | 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. |
| statusStrip | ReactNode | undefined | – | The status strip (StatusStrip), at the end of the bar. |
| actions | ReactNode | undefined | – | Controls after the status strip, such as a screen's night switch. |
| className | string | undefined | – | Classes added to the root's own. |
| testId | string | undefined | – | Becomes data-testid on the root; the title is ${testId}--title, the alert zone's slot ${testId}--alert-zone. |
| ref | Ref<HTMLDivElement> | undefined | – | The root element. |
Related
- 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.