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

Holds on to the start of the bar; usually a back or a menu action.

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

Header.Trailing

Anchors to the end of the bar, whatever comes before it.

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

It renders a span, not a heading, because a bar is not always a section: the same Header sits on a dialog, on a sheet and on a mobile screen, and only you know which of those deserves an h2. The id is there so the title can name something else — a modal through aria-labelledby, most of the time.

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>
preview
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>{actions}</Header.Trailing>
</Header>
previewtitle only, then title and actions, then actions only
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>
previewa chip outside the slots, two buttons inside one
Invoice 0142
overdue

A long title clamps at two lines

The title takes the space the slots leave and clamps at two lines. Two rather than one because a bar often carries a real sentence — a document name, an invoice subject — and cutting it at the first line usually loses the part that identifies it; two rather than unlimited because a header that keeps growing pushes everything below it, and on a bar that repeats across screens the rhythm goes with it.

preview
An unusually long title that runs past the two lines it is given and has to be cut somewhere sensible
tsx
<Header.Title title={title} className="line-clamp-1" />

<Header.Title title={title} className="truncate" />

<Header.Title title={title} className="line-clamp-none" />

<Header className="items-start">
  <Header.Title title={title} />
  <Header.Trailing>{close}</Header.Trailing>
</Header>
previewcentred, then items-start on the container
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-2 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.