Header

The bar that sits on top of something: a back action, a title, whatever belongs on the far end. Three wrapper slots and no assumptions about what goes in them.

API

Props

proptypedefault
as?ElementType"header"the element or component to render
className?stringmerged over the defaults, so it wins
…restprops of astyped against the chosen element

Header.Leading

proptypedefault
children?ReactNodewhatever belongs on that side of the bar
className?stringmerged over the defaults

Header.Trailing

proptypedefault
children?ReactNodewhatever belongs on that side of the bar
className?stringmerged over the defaults

Header.Title

proptypedefault
titleReactNodethe text of the bar
id?stringso another element can reference it, as aria-labelledby does
className?stringmerged over the defaults

Examples

Usage

tsx
<Header>
  <Header.Leading>
    <Button aria-label="back" onClickBlur={goBack}>
      <Button.Icon icon={<MdArrowBack />} />
    </Button>
  </Header.Leading>
  <Header.Title title="Release notes" />
  <Header.Trailing>
    <Button aria-label="close" onClickBlur={close}>
      <Button.Icon icon={<MdClose />} />
    </Button>
  </Header.Trailing>
</Header>
Release notes

Mount only what you need

Every slot is optional, and the bar arranges itself around the ones that exist. With no leading slot the title starts at the edge; with no title the trailing slot still anchors to the end.

tsx
<Header>
  <Header.Title title="Invoices" />
</Header>

<Header>
  <Header.Title title="Invoices" />
  <Header.Trailing>
    <Button aria-label="menu" onClickBlur={openMenu}>
      <Button.Icon icon={<MdMenu />} />
    </Button>
  </Header.Trailing>
</Header>

<Header>
  <Header.Leading>
    <Button aria-label="back" onClickBlur={goBack}>
      <Button.Icon icon={<MdArrowBack />} />
    </Button>
  </Header.Leading>
  <Header.Trailing>
    <Button aria-label="close" onClickBlur={close}>
      <Button.Icon icon={<MdClose />} />
    </Button>
  </Header.Trailing>
</Header>
Invoices
Invoices

The slots are wrappers

They take children, not a prop, so what goes in them is entirely yours: two buttons on the trailing side, a chip next to the title, a select instead of a close. And content that belongs to no slot simply sits between them, on the same row.

tsx
<Header>
  <Header.Title title="Invoice 0142" />
  <Chip active>overdue</Chip>
  <Header.Trailing>
    <Button aria-label="share" onClickBlur={share}>
      <Button.Icon icon={<MdShare />} />
    </Button>
    <Button aria-label="delete" onClickBlur={remove}>
      <Button.Icon icon={<MdDelete />} />
    </Button>
  </Header.Trailing>
</Header>
Invoice 0142
overdue

A long title clamps at two lines

tsx
<Header>
  <Header.Leading>{back}</Header.Leading>
  <Header.Title title={longTitle} />
  <Header.Trailing>{close}</Header.Trailing>
</Header>

<Header className="items-start">
  <Header.Leading>{back}</Header.Leading>
  <Header.Title title={longTitle} />
  <Header.Trailing>{close}</Header.Trailing>
</Header>
An unusually long title that runs past the two lines it is given and has to be cut somewhere sensible
An unusually long title that runs past the two lines it is given and has to be cut somewhere sensible

Details

How the anchoring works

The trailing slot carries an automatic left margin, which is what pushes it to the end regardless of what comes before. Two such slots in a row would split the free space between them and drift apart, so a slot that follows another drops that margin — the first one anchors, the rest queue up beside it.

tsx
export const headerTrailingClassName = `ml-auto flex shrink-0 items-center gap-2
[[data-margo-header-slot]+&]:ml-0`;

It is the same mechanism as the slots of Item, and it is CSS: nothing is measured and the component never looks at its own children.

What the bar renders

tsx
<Header as="div" className="border-b-margo border-border">
  <Header.Title id={titleId} title="Delete invoice" />
</Header>

The root defaults to a header element, which is right for a page or a section. Inside a dialog, where the landmark would be noise, render it as a div.

Changing the clamp

It is a default, not a rule. The class is merged, so a single line, a hard truncation or no clamp at all are one className away — and the same is true of the bar itself: with a title on two lines you may want the actions aligned to the top rather than centred on the block, which is items-start on the Header. The kit picks what is right most of the time and stays out of the way for the rest.