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

With TimeWarp.Skills:

dnx TimeWarp.Skills -- add tw-blazor-layout https://timewarp.software/

Add a harness target and skills sync (see the TimeWarp.Skills page). 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). The brand theme is not one of them: it is set on the first server render (App.razor's <body data-theme-color … data-theme>), never applied from OnAfterRenderAsync — see tw-blazor-css-strategy rule 8. Guard anything else 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. A shell tool that needs the same column (Ask) owns it while it is open and the page Aside returns when that tool closes. The layout has one aside area.
  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

The shell owns the notification region

Every page gets exactly one notification region, owned by the shell — not by pages, cards, or feature components. Both page shells (the full shell and its focused variant) render the notification-region host directly below the page title/breadcrumb and above the first card. The host paints a state store that holds the current bars; it is the single place an operation outcome (success or failure) becomes visible.

The nav menu and any other destination surface (a command palette, a sitemap) read one generated list of pages — never a second hand-copied route list.

Reference implementation (timewarp-architecture)

Concrete instance of the pattern in this repo: