Page structure

The structural component for what goes inside <main>.

.page is a flex-column that owns the rhythm between its top-level children via --page-section-gap. The header and sections carry no outer margin themselves. No JS, pure CSS.

The page wrapper

Drop a .page inside .app-shell__main. Top-level children stack vertically with the section gap.

First section content.

Second section content.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <header class="page__header">
    <div class="page__title">
      <h1 class="h3">Customers</h1>
    </div>
  </header>
  <section class="page__section">
    <p class="text-muted-foreground">First section content.</p>
  </section>
  <section class="page__section">
    <p class="text-muted-foreground">Second section content.</p>
  </section>
</div>

.page__header is a flex row with a .page__title slot on the leading edge and a .page__action slot on the trailing edge. Both slots are passthrough. Title takes a heading with an optional subtitle, actions takes any button row.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <header class="page__header">
    <div class="page__title">
      <h1 class="h3">Customers</h1>
      <p class="text-muted-foreground">All accounts in this workspace.</p>
    </div>
    <div class="page__action">
      <button type="button" class="btn btn--outline btn--neutral"><i data-lucide="download"></i>Export</button>
      <button type="button" class="btn btn--primary"><i data-lucide="plus"></i>Add customer</button>
    </div>
  </header>
</div>

Without actions

Drop the action slot. The title block sits alone.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <header class="page__header">
    <div class="page__title">
      <h1 class="h3">Settings</h1>
      <p class="text-muted-foreground">Manage your account, billing, and integrations.</p>
    </div>
  </header>
</div>

Wraps on narrow widths

The header is flex-wrap: wrap. When the viewport gets too narrow for the title and action slot to share a row, the actions drop below.

<header class="page__header">
  <div class="page__title">
    <h1 class="h3">Customers</h1>
    <p class="text-muted-foreground">All accounts in this workspace.</p>
  </div>
  <div class="page__action">
    <button type="button" class="btn btn--primary">Add customer</button>
  </div>
</header>

Sections

.page__section is a flex-column for each block inside the page. The .page parent supplies the gap between sections; each section supplies its own inner gap via --page-section-inner-gap.

Overview

Headline stats and the period selector live here.

Recent activity

A timeline of the last events on this account.

Quick actions

Shortcuts to the most-used flows.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <section class="page__section">
    <h2 class="page__section-title">Overview</h2>
    <p class="text-muted-foreground">Headline stats and the period selector live here.</p>
  </section>
  <section class="page__section">
    <h2 class="page__section-title">Recent activity</h2>
    <p class="text-muted-foreground">A timeline of the last events on this account.</p>
  </section>
  <section class="page__section">
    <h2 class="page__section-title">Quick actions</h2>
    <p class="text-muted-foreground">Shortcuts to the most-used flows.</p>
  </section>
</div>

With section header + actions

For sections that need their own action row, wrap the title in .page__section-header and reuse .page__action as the slot. Same flex recipe at a tighter scale.

Active customers

1,284 accounts.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <section class="page__section">
    <header class="page__section-header">
      <h2 class="page__section-title">Active customers</h2>
      <div class="page__action">
        <button type="button" class="btn btn--outline btn--neutral btn--sm">Filter</button>
        <button type="button" class="btn btn--primary btn--sm">Invite</button>
      </div>
    </header>
    <p class="text-muted-foreground">1,284 accounts.</p>
  </section>
</div>

Content width

.page is unopinionated about width. Wrap it (or set its own width on a parent) to clamp the content column.

  • Full width (.container-fluid or no container) for dashboards, tables, anything dense.
  • Breakpoint-clamped (.container) for marketing pages, or anything that should match the standard column widths.
  • Custom max-width (40 to 60rem) for settings forms, articles, anything text-heavy where long lines hurt readability.
<!-- Full width -->
<main class="app-shell__main">
  <div class="page container-fluid p-4">...</div>
</main>

<!-- Breakpoint-clamped -->
<main class="app-shell__main">
  <div class="page container p-4">...</div>
</main>

<!-- Custom narrow column for reading -->
<main class="app-shell__main">
  <div class="page p-4" style="margin-inline: auto; max-width: 48rem;">...</div>
</main>

Putting it together

Page header, sections, and a section with its own header, all inside one .page that supplies the rhythm.

Active

1,284 customers.

Inactive

312 customers.

<div class="page p-4 w-100" style="border: 1px solid var(--st-border); border-radius: var(--st-radius); background: var(--st-background);">
  <header class="page__header">
    <div class="page__title">
      <h1 class="h3">Customers</h1>
      <p class="text-muted-foreground">All accounts in this workspace.</p>
    </div>
    <div class="page__action">
      <button type="button" class="btn btn--outline btn--neutral"><i data-lucide="download"></i>Export</button>
      <button type="button" class="btn btn--primary"><i data-lucide="plus"></i>Add customer</button>
    </div>
  </header>
  <section class="page__section">
    <header class="page__section-header">
      <h2 class="page__section-title">Active</h2>
      <div class="page__action">
        <button type="button" class="btn btn--outline btn--neutral btn--sm">Filter</button>
      </div>
    </header>
    <p class="text-muted-foreground">1,284 customers.</p>
  </section>
  <section class="page__section">
    <h2 class="page__section-title">Inactive</h2>
    <p class="text-muted-foreground">312 customers.</p>
  </section>
</div>

Customization

Eight variables retune .page without touching component CSS. Override on .page itself, on a parent scope, or on :root. The cascade scopes the change.

Variable Default Use
--page-section-gap calc(1.5rem * var(--st-density)) Vertical rhythm between top-level children of .page
--page-header-gap calc(1rem * var(--st-density)) Horizontal space between title and action slot in .page__header
--page-title-gap calc(0.25rem * var(--st-density)) Vertical gap between heading and subtitle inside .page__title
--page-action-gap calc(0.5rem * var(--st-density)) Space between buttons inside .page__action
--page-section-inner-gap calc(1rem * var(--st-density)) Vertical gap between header and content inside .page__section
--page-section-header-gap calc(0.75rem * var(--st-density)) Horizontal space between title and action slot in .page__section-header
--page-section-title-size 1.125rem Font size of .page__section-title
--page-section-title-weight 600 Font weight of .page__section-title