Page Composition
Assemble in-app (console) pages from bank scaffolds instead of re-rolling container and section CSS in every app.
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:
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
.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
<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
Headingprimitive. - 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. - headingLevel —
h1/h2/h3(defaulth2). - width — reading column:
sm920px,md1000px,lg1060px (defaultlg).
SectionBlock props
- title — section title, rendered as
h3at 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 autoon a page root, you took a wrong turn. - Pick the width deliberately —
smfor dense lists,mdfor mixed content,lgfor wide grids. - One header per page — the page kicker/title/subtitle belong to
ConsolePage; section-level titles belong toSectionBlock. - 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.