Page is the outermost layout for a route, setting where the title and page-level actions sit and how wide the content column is.

A page consists of the following components:

  • Page
    • The container for the entire page.
  • PageHeader
    • The container, renders a header tag.
  • PageHeaderIconTile
    • An icon tile displayed to the left of the title.
  • PageHeaderLabel
    • A status label displayed to the right of the title.
  • PageHeaderTitle
    • The title of the page header.
  • PageHeaderTitleAction
    • A slot for an inline action (e.g. an edit button) displayed to the right of the title (after PageHeaderLabel if present).
  • PageHeaderActions
    • The actions to be displayed on the right side of the page header. It should contain Button components.
  • PageHeaderDescription
    • The description of the page header.
  • PageHeaderTabs
    • The tabs to be displayed below the page header. It should contain a Tabs component.
  • PageHeaderExtra
    • A slot for page-level content the header has no dedicated area for, between the description and the tabs.
  • PageBody
    • The container for the body of the page.
  • PageSection
    • A band inside the body that stacks with its siblings, with its own content width and an optional divider.
  • PageRail
    • A column beside the body's content, optionally sticky, on either side.
  • PageBodyContent
    • Wraps the sections a rail stands beside. Only needed when the body has a rail.

You can mix and match these components to create a simple or complex page header. At minimum you should have a PageHeader, PageHeaderTitle, and PageBody.

Importing

The components can be imported via:

import {
	Page,
	PageHeader,
	PageHeaderIconTile,
	PageHeaderLabel,
	PageHeaderTitle,
	PageHeaderTitleAction,
	PageHeaderActions,
	PageHeaderDescription,
	PageHeaderTabs,
	PageHeaderExtra,
	PageBody,
	PageSection,
} from '@customerio/pluma-components/react';

Example

Loading editor

Common layouts

Page composes into a few recurring layouts. In each, the PageHeader is full-bleed with a divider separating it from the body (use isFullScreen); what changes is the body width.

Set the content width once on the Page with bodyContentWidth — every PageBody and PageSection inherits it, so the layout holds together even when the header and body are assembled in different files. An individual PageBody or PageSection can still override the page default with its own contentWidth.

Wizard

A full-bleed bordered header above a body split into sections — a Stepper, the step content, and the actions. Good for setup flows and long forms, where a narrower reading width keeps each step focused.

See the Wizard layout pattern for the full composition.

Builder / Composer

A full-bleed header above a full-bleed body — for canvas and builder surfaces that use the whole viewport. Use isFullScreen and leave the body at its default, so it inherits the page's full width.

Loading editor

PageHeaderIconTile

An icon tile displayed to the left of the title.

Loading editor

PageHeaderLabel

A status label displayed to the right of the title.

Loading editor

PageHeaderTitleAction

A slot for an inline action displayed to the right of the title (after PageHeaderLabel if present) — typically an edit button or other affordance that operates on the page subject.

Loading editor

PageHeaderExtra

A slot for page-level content the header has no dedicated area for — a Banner about the page as a whole, or a placeholder standing in for one while it loads. It sits below the title row and the description and above the tabs.

Unlike PageHeaderTabs, which bleeds to the header's edges so the tab underline can run the full width, PageHeaderExtra stays inside the header's content band. Put page-level content here rather than in the tabs slot: the tabs slot needs its negative inline margin undone by hand, and the spacing has to be guessed.

The slot is spaced off the title block above and the tabs below by the header's block padding (pageHeaderPaddingBlock), so nothing needs to carry its own margin.

Content that belongs to the page's content rather than to the page itself goes in a PageSection at the top of PageBody instead. The test is whether it would still make sense if the page's tabs switched to another view — a banner about the whole record would, a banner about the table below it would not.

Closing the header

A header with tabs ends in the line the tab list draws under itself, and a full-bleed header draws that line whenever it has no tabs. A header that is neither — one whose last row is a PageHeaderExtra — has no line, and the way to get one used to be rendering an empty PageHeaderTabs for the side effect.

withDivider on the slot draws it instead. It is ignored when the header has tabs, since they draw that line themselves and a header cannot sensibly end in two, and it is redundant on a full-screen header, which already has one.

Loading editor

PageSection

A band inside PageBody that stacks with its siblings — a wizard's Stepper, a page-level banner, a filter bar, the content itself, the trailing actions. Sections are how a page varies its content width down the page: each one takes its own contentWidth, and the rest of the page is unaffected.

A section can never be wider than the body that contains it, so leave PageBody unconstrained and constrain the sections.

Spacing

Sections carry no padding of their own — PageBody already insets the page, and a section adding to it would double the inset. The only block spacing a section owns is the gap to a sibling section (pageSectionGap), so the first and last sections add nothing at the body's edges.

pageSectionGap is the space between any two adjacent things, and a divider counts as one of them. An undivided boundary is a single gap; a divided one is a gap above the line and a gap below.

Dividers

divider draws a line on the boundary between a section and the next. It never renders after the last section, so it can't leave a trailing line.

  • contained — spans the section's content column. Use it when adjacent sections share a content width, so the line aligns with the text.
  • extended — spans the body. Use it when the sections differ in width, or when the line marks a change of page region rather than a break in content.

Either mode clears the content by one gap on both sides, so the line sits centred between the two sections whichever you pick.

Loading editor

PageRail

A column beside the body's content — a wizard's vertical Stepper, a table of contents, a summary. It stands beside every section in the body, so it can stay in view while a long page scrolls, and is as wide as its content needs.

Put the rail inside PageBody and wrap the sections in a PageBodyContent.

Loading editor

Which side the rail takes

Write the rail before PageBodyContent and it sits on the leading side; write it after and it sits on the trailing side. There's no placement prop, so a rail can't be drawn on one side while being read from the other.

Lead with content that helps someone move through the page, like a Stepper or a table of contents — it's read first, ahead of what it describes. Put content that comments on the page, like a summary, after.

A body can hold a rail on each side. Two is rarely right — it leaves the content squeezed between them.

Content width

The content keeps the width its preset asks for and the rail takes the space beside it. Where there isn't room for both, the content gives way:

  • narrow — the content stays centred and the rail sits in the space beside it.
  • full — the rail always eats into the content.
  • standard — the rail eats in until the viewport is wide enough to centre the content clear of it.

PageBody spans the page when it holds a rail, so bodyContentWidth and contentWidth size the content column rather than the body. A PageSection can't be wider than that column, which is why divider="extended" draws the same line as contained here.

Below the narrowest width that fits both, the content stops shrinking and the page scrolls sideways.

Sticky rails

isSticky keeps the rail in view while the content beside it scrolls, and releases it at the end of the content.

The rail sticks to whatever the app scrolls, so two things are worth knowing:

  • An overflow other than visible between the rail and that scroll container disables sticky positioning, silently.
  • pageRailStickyInsetBlockStart clears fixed chrome above the page. It defaults to the body's block padding.

Keep a sticky rail short enough to fit the viewport. A taller one puts its own lower end out of reach while it's stuck.

Accessibility

PageRail renders a div, which suits a Stepper or anything else that brings its own semantics. Pass as="nav" with an aria-label when the rail holds links into the page, or as="aside" when its content is genuinely tangential.

Because the side comes from where the rail is written, reading order and visual order always agree.

PageBreadcrumbs is deprecated and will be removed in the next major release. It still renders — it lives as a sibling of PageHeader inside Page, not inside the header grid — so existing pages keep working without changes.

For new code, render the standalone Breadcrumbs component above the page header yourself instead.

import { Breadcrumbs, BreadcrumbsItem } from '@customerio/pluma-components/react';

<Breadcrumbs>
	<BreadcrumbsItem href="/">Home</BreadcrumbsItem>
	<BreadcrumbsItem href="/settings">Settings</BreadcrumbsItem>
</Breadcrumbs>
<Page>
	<PageHeader>
		<PageHeaderTitle>Page Title</PageHeaderTitle>
	</PageHeader>
	<PageBody>This is the body of the page.</PageBody>
</Page>

Full screen page

The Page component can be full screen by passing the isFullScreen prop.

Loading editor

Page loading state

The Page component can be in a loading state by passing the isLoading prop.

Loading editor

Page body loading state

When the page is full screen or has tabs, the page looks better with just the body in a loading state. This gives the user more of an anchor for the page. You can pass the isLoading prop to the PageBody component to achieve this.

Loading editor

API

PlumaPage extends Box

Sets the default content width for every PageBody and PageSection on the page, so the width can be configured once on the page rather than repeated on each one. A contentWidth prop on an individual PageBody or PageSection wins over this.

See PageBody's contentWidth for the available presets.

Whether the page should be full screen.

Whether the page is loading.

Default:1

The level of the heading (which maps to h1-h6 tags)

Whether the header closes with a divider below this slot, the line a PageHeaderTabs would otherwise have to be rendered — empty — to draw.

Ignored when the header has tabs, since the tabs draw that line themselves and a header cannot sensibly end in two. A full-screen header already draws it whenever it has no tabs, so this is for the header that is not full screen.

Default:'teal'

The color of the icon tile.

The name of the icon to render.

Default:true

Whether the label should render in a bold font weight

Default:grey

The visual style of the label. Note: When using status indicators, only red, yellow, green, blue, purple, and grey colors are supported.

Default:false

Whether the label should show a status indicator colored dot

Constrains the body's content width. When omitted, the body inherits the page width (standard, or full-bleed when the page isFullScreen).

  • standard — the standard page width (min 950px, max 1850px), even under a full-screen header.
  • narrow — max 800px, no minimum width.
  • full — full bleed: stretches to the page with no minimum width. Use it to break a single body out of a narrower page default.

Whether the page body is loading.

Constrains this section's content width, independently of the body and of the other sections. When omitted, the section fills the body.

See PageBody's contentWidth for the available presets. A section can never be wider than the body that contains it — a preset whose minimum exceeds the body clamps to the body rather than overflowing it — so leave the body unconstrained and set the width per section.

Default:'none'

Draws a line on the boundary between this section and the next. Never renders after the last section, so it can't leave a trailing line.

  • contained — spans this section's content column.
  • extended — spans the body, for a break between page regions rather than between pieces of content. Draws the same line as contained in a body that has a PageRail.

Whether the rail stays in view while the content beside it scrolls, released at the end of the content.

The rail sticks to whatever the app scrolls, so an overflow other than visible between the two disables it.