Skip to content
design
Components

Page header

A page's label, title and subtitle, with an optional bar under them and an option to stay at the top.

On this page
00:00
Watch handed from Tomás to Maya. Course 302°T, 6.4 kn under main and genoa.
00:40
Cabo de Palos light abeam to port, 11 NM. Bearing steady.
01:15
Wind backing southwest, 18 kn. Reefed the main to the first reef.
02:00
Position 37°18.4′N 000°41.2′W. Log 1 412.6 NM. Baro 1008 hPa, falling slowly.
02:30
AIS: tanker Nordic Aurora crossing ahead, CPA 1.8 NM at 02:52. No action needed.
03:10
Engine on to charge the house bank, 38 % state of charge. 1 800 rpm.
03:50
Engine off. House bank 61 %. Sea temperature 21.8 °C.
04:00
Watch handed from Maya to Ingrid. All well.
The logbook's header with its label, title, long subtitle and a bar of badges and actions, staying at the top as the watch entries scroll under it.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { DescriptionList, type DescriptionListItem } from "@bosunkit/components/description-list";
import { PageHeader } from "@bosunkit/components/page-header";
import { Download, Plus } from "lucide-react";
import type { ReactElement } from "react";

export const description: string =
  "The logbook's header over a scrolling page: label, title and a long subtitle, a context bar of badges and " +
  "actions, and the header staying at the top as the watch entries scroll under it.";

interface LogEntry {
  readonly time: string;
  readonly text: string;
}

const ENTRIES: readonly LogEntry[] = [
  { time: "00:00", text: "Watch handed from Tomás to Maya. Course 302°T, 6.4 kn under main and genoa." },
  { time: "00:40", text: "Cabo de Palos light abeam to port, 11 NM. Bearing steady." },
  { time: "01:15", text: "Wind backing southwest, 18 kn. Reefed the main to the first reef." },
  { time: "02:00", text: "Position 37°18.4′N 000°41.2′W. Log 1 412.6 NM. Baro 1008 hPa, falling slowly." },
  { time: "02:30", text: "AIS: tanker Nordic Aurora crossing ahead, CPA 1.8 NM at 02:52. No action needed." },
  { time: "03:10", text: "Engine on to charge the house bank, 38 % state of charge. 1 800 rpm." },
  { time: "03:50", text: "Engine off. House bank 61 %. Sea temperature 21.8 °C." },
  { time: "04:00", text: "Watch handed from Maya to Ingrid. All well." },
];

/** The watch entries as terms and values: each time beside what was logged. */
const LOG: readonly DescriptionListItem[] = ENTRIES.map((entry: LogEntry): DescriptionListItem => ({
  id: entry.time,
  term: entry.time,
  value: entry.text,
}));

function ignore(): void {
  // The example adds and exports nothing.
}

export default function Showcase(): ReactElement {
  return (
    <div
      style={{
        width: "100%",
        height: 320,
        overflowY: "auto",
        border: "1px solid var(--border)",
        borderRadius: "var(--radius-md)",
        padding: "0 var(--space-lg) var(--space-lg)",
      }}
    >
      <PageHeader
        eyebrow="bosun.bot · SV Althea"
        title="Logbook"
        subtitle="Passage from Palma de Mallorca to Gibraltar, day 2 of 4. Every entry is in UTC."
        sticky
        contextBar={
          <div
            style={{
              display: "flex",
              alignItems: "center",
              gap: "var(--space-sm)",
              flexWrap: "wrap",
              padding: "var(--space-sm) 0 var(--space-md)",
              borderBottom: "1px solid var(--border)",
            }}
          >
            <Badge variant="green">Underway</Badge>
            <Badge variant="blue">Maya Lindqvist on watch</Badge>
            <Badge variant="amber">Baro falling</Badge>
            <span style={{ flex: 1 }} />
            <Button size="sm" variant="ghost" onClick={ignore}>
              <Download size={14} />
              Export
            </Button>
            <Button size="sm" variant="primary" onClick={ignore}>
              <Plus size={14} />
              Add entry
            </Button>
          </div>
        }
      />
      <div style={{ paddingTop: "var(--space-md)" }}>
        <DescriptionList items={LOG} />
      </div>
    </div>
  );
}

A page header opens a page. A small label sits above the title, such as the product name, and a subtitle beside the title says what the page is for. The caller translates the title and the subtitle. contextBar holds a row under the title for the page's badges, filters or actions. With sticky the header stays at the top while the page scrolls.

Examples

Title

A label above a title, the least a page header holds.
Code
import { PageHeader } from "@bosunkit/components/page-header";
import type { ReactElement } from "react";

export const description: string = "A label above a title, the least a page header holds.";

export default function Title(): ReactElement {
  return <PageHeader eyebrow="bosun.bot" title="Settings" />;
}

With subtitle

A subtitle beside the title, saying what the page is for.
Code
import { PageHeader } from "@bosunkit/components/page-header";
import type { ReactElement } from "react";

export const description: string = "A subtitle beside the title, saying what the page is for.";

export default function WithSubtitle(): ReactElement {
  return <PageHeader eyebrow="bosun.bot" title="Settings" subtitle="Vessel profile and configuration." />;
}

Sticky

A header that stays at the top while the page scrolls, with a badge in the bar under the title.
Code
import { Badge } from "@bosunkit/components/badge";
import { PageHeader } from "@bosunkit/components/page-header";
import type { ReactElement } from "react";

export const description: string =
  "A header that stays at the top while the page scrolls, with a badge in the bar under the title.";

export default function Sticky(): ReactElement {
  return (
    <PageHeader
      eyebrow="bosun.bot"
      title="Spoken media"
      subtitle="Offline podcasts and chapter-aware audiobooks."
      sticky
      contextBar={<Badge variant="amber">Podcast offline</Badge>}
    />
  );
}

Title, context and actions

Give the page one primary title that describes the destination. Use the subtitle for purpose or scope, and contextBar for filters, metadata and actions that apply to the whole page. Put actions for a single panel inside that panel. A report shell's title and description should appear once before its report metadata.

At narrow widths, let context wrap while keeping the title readable. A sticky page header needs room below the application header and must not cover anchor targets or focused controls. Check long titles, a missing subtitle, and a context row with several controls in the actual shell.

Props

PropTypeDefaultDescription
eyebrowrequiredstring–The small line above the title, such as the product name.
titlerequiredstring–The page's title, translated by the caller.
subtitlestring | undefined–A line beside the title, translated by the caller.
stickyboolean | undefinedfalseWhether the header stays at the top of the page while it scrolls.
contextBarReactNode | undefined–A bar under the title for the page's badges, filters or actions.
classNamestring | undefined–Classes added to the header.
  • Typography: the type the label, title and subtitle use.