How to structure Blazor app chrome with the "empty layout + cascaded page-component shell" pattern — keep LayoutComponentBase empty and put header/nav/content/aside/footer in ONE shell component that pages wrap their content in and that cascades itself. Use when designing a Blazor app's layout/navigation, deciding where chrome belongs, building a layout shell, or when chrome must react to a state store or per-navigation lifecycle that a layout can't provide.

Maintained in timewarp-architecture · Canonical file: SKILL.md

Install

npx skills add TimeWarpEngineering/timewarp-architecture --skill blazor-layout

Or copy the SKILL.md into your agent's skills directory.


Blazor app shell: empty layout + cascaded page-component shell

A technique for where app chrome (header, nav, content area, aside, footer) lives in a Blazor app.

The pattern: keep the routed layout (LayoutComponentBase) almost empty, and put all chrome in a single shell component that (a) inherits your app's state/base component, (b) renders the layout zones, (c) cascades itself, and (d) is wrapped around each page's content. Pages don't declare chrome; they render their body inside <Shell>.

Why not just put chrome in the layout?

LayoutComponentBase is the obvious place, but it has two limits that bite real apps:

A shell that is a normal (state-aware) component solves both: it re-renders with your store, has per-instance lifecycle, and takes Title/Aside/etc. as parameters. You then make it reachable to descendants by cascading it, and you keep the routed layout empty so it doesn't fight the shell.

How to build it

  1. Empty layout. Your LayoutComponentBase renders just @Body (plus any genuinely layout-root, run-once concerns — e.g. applying a theme). Guard anything that uses JS interop so it only runs when interactive (not during server prerender). No header/nav/footer here.
  2. Shell component. A normal component that inherits your state/base component (not LayoutComponentBase). It:
    • renders the chrome zones using your UI library's layout primitive (header / navigation / content / aside / footer);
    • declares [Parameter]s for per-page inputs (Title, ChildContent, optional Aside);
    • <CascadingValue Value=@this> wraps its tree so descendants can reach the shell;
    • renders @ChildContent in the content zone, and conditionally renders the aside zone only when Aside is supplied.
  3. Pages wrap their content in the shell: <Shell Title="…">…page body…</Shell>. Routing stays a separate concern (your @page/route attribute), independent of the shell.

Pitfalls

Reference implementation (timewarp-architecture)

Concrete instance of the pattern in this repo: