Skip to content
design
Components

Section heading

The small capitals heading over a group of rows, cards or settings, with an icon, a count and an action.

On this page

Due soon 4

Service 2

Liferaft serviceOverdue by 12 days
Raw-water impellerDue in 38 engine hours

Documents 2

Insurance certificateExpired 3 days ago
Ship station licenseValid until March 2029
A maintenance page: the page's group heading, and two groups each with an icon, a count and an action.
Code
import { Badge } from "@bosunkit/components/badge";
import { Button } from "@bosunkit/components/button";
import { DueChip } from "@bosunkit/components/due-chip";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { FileText, LifeBuoy, Plus, Wrench } from "lucide-react";
import { useId, type CSSProperties, type ReactElement } from "react";

export const description: string =
  "SV Althea's maintenance page: a level 2 heading over the page's two groups, each a level 3 heading with an icon, " +
  "a count and an action, naming the section it heads.";

const ROW: CSSProperties = {
  display: "flex",
  alignItems: "center",
  justifyContent: "space-between",
  gap: "var(--space-sm)",
  padding: "var(--space-sm) 0",
  borderTop: "1px solid var(--border)",
};

export default function Showcase(): ReactElement {
  const serviceId: string = useId();
  const documentsId: string = useId();
  return (
    <div style={{ display: "flex", flexDirection: "column", gap: "var(--space-lg)", width: "100%", maxWidth: 560 }}>
      <SectionHeading level={2} count={4}>
        Due soon
      </SectionHeading>
      <section aria-labelledby={serviceId}>
        <SectionHeading
          level={3}
          id={serviceId}
          icon={<Wrench />}
          count={2}
          action={
            <Button size="sm" variant="ghost">
              <Plus size={14} aria-hidden="true" />
              Add service
            </Button>
          }
        >
          Service
        </SectionHeading>
        <div style={ROW}>
          <span style={{ display: "inline-flex", alignItems: "center", gap: "var(--space-xs)" }}>
            <LifeBuoy size={14} aria-hidden="true" />
            Liferaft service
          </span>
          <DueChip priority="warning" label="Overdue by 12 days" />
        </div>
        <div style={ROW}>
          <span>Raw-water impeller</span>
          <DueChip priority="caution" label="Due in 38 engine hours" />
        </div>
      </section>
      <section aria-labelledby={documentsId}>
        <SectionHeading
          level={3}
          id={documentsId}
          icon={<FileText />}
          count={2}
          action={
            <Button size="sm" variant="ghost">
              <Plus size={14} aria-hidden="true" />
              Add document
            </Button>
          }
        >
          Documents
        </SectionHeading>
        <div style={ROW}>
          <span>Insurance certificate</span>
          <DueChip priority="warning" label="Expired 3 days ago" />
        </div>
        <div style={ROW}>
          <span>Ship station license</span>
          <Badge variant="dim">Valid until March 2029</Badge>
        </div>
      </section>
    </div>
  );
}

A section heading names a group of rows, cards or settings in small mono capitals in the dim text color, so the content under it carries the weight. It is a real heading element at the level the caller sets, from 2 to 6, so a screen reader lists the groups in the page's outline. The look is the same at every level; the level follows the page's structure, never the size wanted. The heading takes an icon before the name, a count after it and an action at the end of the row. Its sizes are in em, so it follows the size class around it.

01A heading, not a styleThe group name is an h2 to h6 that the caller places in the outline, so the groups are headings a screen reader can move between.
02Quiet over its contentSmall dim capitals name the group and leave the rows under them to carry the weight.
03The action beside the nameA control that adds to the group sits at the end of its heading row, outside the heading, where the crew look for it.

Examples

Heading

Engine

A group's name as a level 3 heading, in small mono capitals.
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import type { ReactElement } from "react";

export const description: string = "A group's name as a level 3 heading, in small mono capitals.";

export default function Heading(): ReactElement {
  return <SectionHeading level={3}>Engine</SectionHeading>;
}

With an icon

The icon is decorative: it sits inside the heading and is hidden from assistive technology, so the heading's name is the group's name alone.

Anchor watch

An icon before the name, hidden from assistive technology.
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import { Anchor } from "lucide-react";
import type { ReactElement } from "react";

export const description: string = "An icon before the name, hidden from assistive technology.";

export default function WithIcon(): ReactElement {
  return (
    <SectionHeading level={3} icon={<Anchor />}>
      Anchor watch
    </SectionHeading>
  );
}

With a count

The count is drawn in a quiet chip with tabular figures and read with the name, a zero included, so the crew hear "Overdue 3" when they move to the heading.

Overdue 3

The number of items in the group after its name, read with it as "Overdue 3".
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import type { ReactElement } from "react";

export const description: string = 'The number of items in the group after its name, read with it as "Overdue 3".';

export default function WithCount(): ReactElement {
  return (
    <SectionHeading level={3} count={3}>
      Overdue
    </SectionHeading>
  );
}

With an action

The action sits at the end of the row, outside the heading, so the heading's name stays the group's name and the action keeps its own.

Crew aboard 4

A button at the end of the row that adds to the group, outside the heading.
Code
import { Button } from "@bosunkit/components/button";
import { SectionHeading } from "@bosunkit/components/section-heading";
import { Plus, Users } from "lucide-react";
import type { ReactElement } from "react";

export const description: string = "A button at the end of the row that adds to the group, outside the heading.";

export default function WithAction(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 420 }}>
      <SectionHeading
        level={3}
        icon={<Users />}
        count={4}
        action={
          <Button size="sm" variant="ghost">
            <Plus size={14} aria-hidden="true" />
            Add crew member
          </Button>
        }
      >
        Crew aboard
      </SectionHeading>
    </div>
  );
}

Levels

Set level from the page's outline: 2 under the page title, 3 under a level 2 group. The heading looks the same at every level.

Passage to A Coruña

412 NM from Brest, departing 06:00 UTC.

Tidal gates

Raz de Sein at slack water, 08:40 UTC.

Ports of refuge

Camaret, Audierne, Gijón.

A level 2 heading over two level 3 groups: the levels follow the page's outline, and the look stays the same.
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import type { CSSProperties, ReactElement } from "react";

export const description: string =
  "A level 2 heading over two level 3 groups: the levels follow the page's outline, and the look stays the same.";

const TEXT: CSSProperties = { margin: "var(--space-xs) 0 var(--space-md)", fontSize: 14 };

export default function Levels(): ReactElement {
  return (
    <div style={{ width: "100%", maxWidth: 420 }}>
      <SectionHeading level={2}>Passage to A Coruña</SectionHeading>
      <p style={TEXT}>412 NM from Brest, departing 06:00 UTC.</p>
      <SectionHeading level={3}>Tidal gates</SectionHeading>
      <p style={TEXT}>Raz de Sein at slack water, 08:40 UTC.</p>
      <SectionHeading level={3}>Ports of refuge</SectionHeading>
      <p style={TEXT}>Camaret, Audierne, Gijón.</p>
    </div>
  );
}

Naming a section

Give the heading an id and point the section's aria-labelledby at it, so the section becomes a region named by its heading.

Tanks 3

Diesel 212 L, water 365 L, holding 18 %.

The heading's id names the section it heads, so a screen reader lists the section as the Tanks region.
Code
import { SectionHeading } from "@bosunkit/components/section-heading";
import { Droplets } from "lucide-react";
import { useId, type ReactElement } from "react";

export const description: string =
  "The heading's id names the section it heads, so a screen reader lists the section as the Tanks region.";

export default function NamesASection(): ReactElement {
  const headingId: string = useId();
  return (
    <section aria-labelledby={headingId} style={{ width: "100%", maxWidth: 420 }}>
      <SectionHeading level={3} id={headingId} icon={<Droplets />} count={3}>
        Tanks
      </SectionHeading>
      <p style={{ margin: "var(--space-xs) 0 0", fontSize: 14 }}>Diesel 212 L, water 365 L, holding 18 %.</p>
    </section>
  );
}

Props

PropTypeDefaultDescription
levelrequiredNestedHeadingLevel–The heading element's level, so the group sits in the page's outline: 2 under the page title, and so on.
childrenrequiredReactNode–The group's name, as display text; drawn in small capitals.
iconReactNode | undefined–An icon before the name; it is decorative, since the name says what the group is.
countnumber | undefined–How many items the group holds, drawn after the name and read with it.
actionReactNode | undefined–A control at the end of the row, such as a button that adds to the group or opens all of it.
idstring | undefined–The heading element's id, for a section that names itself with aria-labelledby.
classNamestring | undefined–Classes added to the root.
testIdstring | undefined–Becomes data-testid on the root; the heading is ${testId}--heading.
refRef<HTMLHeadingElement> | undefined–The heading element.
  • Card: the surface whose own title names a smaller group.
  • Section divider: the rule that groups alert cards by urgency.
  • Description list: the terms and values a group often holds.
  • Typography: the mono capitals the heading is set in.