Why this pattern exists

Every in-app page needs the same skeleton: a chassis with a rail and topbar, a centered reading column with padding and vertical rhythm, and titled sections. When each app re-invents that skeleton with local CSS, the ecosystem drifts — inconsistent widths, spacing and heading sizes — and cleaning it up costs entire sessions.

The bank ships three composable scaffolds so a page is filled, never re-styled. Reach for these before writing any container-level CSS.

The three layers

Saas.ConsoleShell — Chassis

The app frame: persistent rail (logo, nav, bottom slot), mobile drawer with hamburger, and a topbar (title, search, actions). Renders its page content through the children slot. One per authenticated layout.

Templates.ConsolePage — Page

A single content page: the standard header (kicker → title → subtitle via AppPageHeader) plus the centered reading column (max-width, padding, vertical rhythm). Goes inside the shell's children.

Templates.SectionBlock — Section

A titled section: optional kicker, h3 title, subtitle and a right-aligned actions slot, then its content. Repeat it for each block of the page.

Composition

Nest them: ConsoleShell › ConsolePage › SectionBlock*. Everything app-specific lives as children — cards, grids, tables — never as new container CSS.

Anatomy

A typical page composes cleanly from the three layers:

tsx
import { Saas, Templates } from "@avnir/ui";

// Chassis — usually in the app's authenticated layout
<Saas.ConsoleShell logo={logo} navItems={nav} title="MUZIDEV">
  {/* Page — one per route */}
  <Templates.ConsolePage
    width="md"                       // sm 920 · md 1000 · lg 1060 (default lg)
    eyebrow="TOOLS · REFERENCES"
    title="Ressources"
    subtitle="Everything that supports your progression."
    subtitleMaxWidth="64ch"
  >
    {/* Sections */}
    <Templates.SectionBlock
      title="Downloads"
      description="Practical deliverables released along the way."
    >
      <div className="ressources-cats">{/* app-specific content */}</div>
    </Templates.SectionBlock>

    <Templates.SectionBlock title="Recommended tools">
      {/* … */}
    </Templates.SectionBlock>
  </Templates.ConsolePage>
</Saas.ConsoleShell>

Do & Don't

The whole point is to delete container CSS, not to move it. If you catch yourself writing the pattern below, replace it with a scaffold.

Don't — re-roll the container

css
.my-page {
  display: flex;
  flex-direction: column;
  gap: var(--gap-xl);
  padding: var(--gap-xl);
  max-width: 1000px;
  margin: 0 auto;
  width: 100%;
}
.my-page-section__head { /* … */ }
.my-page-section__title { font-size: var(--text-h6); }

Do — fill a scaffold

tsx
<Templates.ConsolePage width="md" title="My page">
  <Templates.SectionBlock title="My section">
    {/* only app-specific content here */}
  </Templates.SectionBlock>
</Templates.ConsolePage>

ConsolePage props

  • title — page title (required). Rendered via the Heading primitive.
  • eyebrow — small-caps kicker above the title (e.g. "GLOSSARY · 250 TERMS"). Accepts a ReactNode.
  • subtitle — descriptive line under the title.
  • subtitleMaxWidth — cap a long subtitle (e.g. "64ch") so it stays readable.
  • headingLevelh1 / h2 / h3 (default h2).
  • width — reading column: sm 920px, md 1000px, lg 1060px (default lg).

SectionBlock props

  • title — section title, rendered as h3 at h6 size.
  • description — subtitle under the title.
  • label — small-caps kicker (alternative or complement to the title).
  • actions — content aligned to the right of the header (buttons, filters).

The header only renders when at least one of label, title, description or actions is provided — otherwise SectionBlock is a bare rhythm wrapper.

Guidelines

  • Fill, don't style — a new page needs no container CSS. If you write max-width / margin: 0 auto on a page root, you took a wrong turn.
  • Pick the width deliberatelysm for dense lists, md for mixed content, lg for wide grids.
  • One header per page — the page kicker/title/subtitle belong to ConsolePage; section-level titles belong to SectionBlock.
  • Enrich the bank, not the app— if a page truly needs a variant the scaffold doesn't offer, add it to the bank component (with parity), never as a local override.
  • Guardrails back this up — the no-Tailwind ESLint rule and the CSS budget ratchet keep local container CSS from creeping back.