AppShell
The persistent sidebar + topbar + content frame an authenticated surface sits inside.
<app-shell><app-nav-group><app-nav-link … /></app-nav-group></app-shell><app-shell> is the frame around every authenticated screen: a sidebar of navigation, a sticky topbar, the page content, and an optional footer. Put it in _Layout.cshtml, once — every page renders into its content slot.
It is slot-based, not copy-own. What every product shares is the structure — the breakpoint the sidebar collapses at, the drawer and its scrim and its Escape handling, the 72px topbar, the 88px icon rail, the footer's offset. What differs is only the content, and that arrives as child slots. A product that forked this would re-derive the responsive behaviour and drift from every sibling.
Playground
Every attribute, live. Each change re-renders the real tag helper on the server, so the markup underneath is the exact call that produced the shell above it. Toggling a slot shows or hides that slot's demo content.
collapsed value — it shows what each state looks like rather than an in-place animated toggle. See "Collapse to a rail" below for the fully interactive version.Live preview
The real shell — nav groups with badges and sub-items, a brand, a searchable topbar, KPIs and a scrolling page. Scroll the panel: the sidebar and topbar hold while the content moves, just as they do against the browser window. Drag the frame's bottom edge to resize it.
Dashboard
Your workspace at a glance — updated a moment ago.
Scroll this panel — the sidebar and topbar stay put while the page moves beneath them, which is precisely how the shell behaves against the browser window in a real portal. The topbar carries its divider, the account menu on the right never slides off, and a long nav scrolls independently inside the sidebar.
Below lg (narrow your browser under 992px) the sidebar leaves the layout and becomes an off-canvas drawer: open it from the topbar hamburger, dim the page behind a scrim, and close it with the scrim or the Escape key. Nothing about the content column changes — it simply takes the full width.
Everything here — the nav, the brand, the topbar slots, the footer — is markup you pass as child slots. The shell owns only the structure: the collapse breakpoint, the 72px topbar, the 88px icon rail, the drawer and its focus handling. That is the whole point of not forking it per product.
Show code
<app-shell>
<app-shell-brand><app-icon name="box-seam" size="24" class="text-primary" /><span class="app-sidebar-brand-text">Acme</span></app-shell-brand>
<app-nav-group label="Overview">
<app-nav-link label="Dashboard" icon="layout-dashboard" href="/" active="true" />
<app-nav-link label="Reports" icon="file-text" href="/reports" badge="3" badge-tone="danger" />
<app-nav-link label="Docs" icon="book" href="/docs" trailing-icon="external-link" />
</app-nav-group>
<app-nav-group label="Workspace">
<app-nav-link label="Settings" icon="settings">
<app-nav-link label="General" href="/settings/general" active="true" />
<app-nav-link label="Billing" href="/settings/billing" />
</app-nav-link>
</app-nav-group>
<app-shell-topbar-start><h2>Dashboard</h2></app-shell-topbar-start>
<app-shell-topbar-end>
<app-search placeholder="Search…" size="sm" />
<app-action-icon icon="bell" label="Notifications" />
<app-avatar name="Ravi Kumar" />
</app-shell-topbar-end>
<app-shell-content>@RenderBody()</app-shell-content>
<app-shell-footer><div class="p-2 text-center">© 2026 Acme</div></app-shell-footer>
</app-shell>
Collapse to a rail
Above lg the sidebar collapses to an 88px icon-only rail — from its own collapse toggle (the chevron at the bottom of the sidebar). In the rail each label surfaces as a tooltip on hover or focus, so the icons stay legible.
collapsed="true". Click the chevron at the bottom of the sidebar to expand it, and hover an icon to see its tooltip. Set collapsed from the server to persist the user's last choice.Dashboard
Your workspace at a glance — updated a moment ago.
Scroll this panel — the sidebar and topbar stay put while the page moves beneath them, which is precisely how the shell behaves against the browser window in a real portal. The topbar carries its divider, the account menu on the right never slides off, and a long nav scrolls independently inside the sidebar.
Below lg (narrow your browser under 992px) the sidebar leaves the layout and becomes an off-canvas drawer: open it from the topbar hamburger, dim the page behind a scrim, and close it with the scrim or the Escape key. Nothing about the content column changes — it simply takes the full width.
Everything here — the nav, the brand, the topbar slots, the footer — is markup you pass as child slots. The shell owns only the structure: the collapse breakpoint, the 72px topbar, the 88px icon rail, the drawer and its focus handling. That is the whole point of not forking it per product.
Show code
@* Start as a rail; the bottom toggle expands it *@ <app-shell collapsed="true">…</app-shell>
Focused mode — no sidebar
A full-bleed report or a step-by-step wizard reads better without the nav competing. no-sidebar="true" drops the side column to a single full-width column, keeping the topbar and footer so the user is never lost.
Show code
<app-shell no-sidebar="true">
<app-shell-topbar-start><h2>New invoice</h2></app-shell-topbar-start>
<app-shell-topbar-end><app-button variant="ghost">Cancel</app-button></app-shell-topbar-end>
<app-shell-content>@RenderBody()</app-shell-content>
<app-shell-footer><div class="p-2 text-center">Step 2 of 4</div></app-shell-footer>
</app-shell>
Nav item extras
Beyond label/icon/href, each <app-nav-link> can carry a toned badge, a trailing-icon, and nested <app-nav-link> children — all visible in the Live preview above.
badge-tone tints the count (danger/success/warning); trailing-icon adds a right-aligned glyph after the label; nesting <app-nav-link> makes an expandable parent that opens on load when a child is active. In the rail each label surfaces as a hover/focus tooltip.<app-nav-group label="Overview">
<app-nav-link label="Reports" icon="file-text" href="/reports" badge="3" badge-tone="danger" />
<app-nav-link label="Clients" icon="building-2" href="/clients" badge="New" badge-tone="success" />
<app-nav-link label="Docs" icon="book" href="/docs" trailing-icon="external-link" />
<app-nav-link label="Settings" icon="settings">
<app-nav-link label="General" href="/settings/general" active="true" />
<app-nav-link label="Billing" href="/settings/billing" />
</app-nav-link>
</app-nav-group>
Content slots
Content slots take arbitrary markup — a tag helper cannot take markup as an attribute, so where React has node props Razor has child elements: app-shell-brand, app-shell-sidebar-start, app-shell-sidebar-start-compact (collapsed-rail twin — typically a compact monogram workspace switcher), app-shell-sidebar-footer, app-shell-topbar-start, app-shell-topbar-end and app-shell-footer. The page body itself isn't one of those — it's whatever is left inside <app-shell> that no slot claims, though <app-shell-content> lets you mark it explicitly.
app-shell-topbar-start shrinks and ellipsises a long page title; app-shell-topbar-end is pinned, so the account menu is never pushed off-screen by one.<app-shell>
<app-shell-brand><img src="/logo.svg" alt="Acme" height="26" /></app-shell-brand>
<app-nav-group label="Overview">…</app-nav-group>
<app-shell-sidebar-footer><span class="text-body-secondary">v2.4.1</span></app-shell-sidebar-footer>
<app-shell-topbar-start><h1 class="card-title">Dashboard</h1></app-shell-topbar-start>
<app-shell-topbar-end><app-avatar name="Ravi Kumar" /></app-shell-topbar-end>
<app-shell-content>@RenderBody()</app-shell-content>
<app-shell-footer><div class="p-2 text-center">© Acme</div></app-shell-footer>
</app-shell>
<app-shell> that is not captured by a slot becomes the content, so a layout can put @RenderBody() straight inside..nav-item, .nav-link and .navbar-* are Bootstrap's own class names — a shared package claiming them would silently restyle every consumer's Bootstrap nav, so the shell uses .app-nav-item, .app-sidebar, .app-topbar and so on instead.Rail above lg, drawer below
Two different behaviours at two different sizes, and they are not the same mechanism.
lg the edge button collapses the sidebar to an 88px icon rail. Below lg there is no rail — the sidebar becomes an off-canvas drawer opened by the topbar hamburger, over a scrim, closable with Escape. Narrow the browser under 992px on any preview above to see it switch. Both are wired by webority-ui.js.@* Start collapsed *@ <app-shell collapsed="true">…</app-shell> @* A focused screen — no side nav at all *@ <app-shell no-sidebar="true">…</app-shell>
min-height: 100vh with a sticky sidebar and topbar and a footer fixed to the viewport — put it in _Layout.cshtml once and it fills the window.When to use
Use it when
- Every authenticated screen on a surface — put it in the layout, once.
- The product has more than a handful of destinations.
- You want the same collapse, drawer and topbar behaviour as every sibling product.
Reach for something else when
- It is a public marketing page. → the marketing layout
- It is a sign-in screen — there is no nav to show. → a centred layout
- You need a temporary side panel, not the app nav. → app-sidebar-menu
Every option
The whole surface of the component, one cell per value — the sidebar states, every slot toggle, the nav link's badge/trailing/expandable behaviours, and the edge cases a normal example never shows.
sidebar state — expanded vs collapsed rail
(default) expanded
collapsed="true"
layout — sidebar vs noSidebar
(default) with sidebar
no-sidebar="true" — full-width single column
sidebarStart / sidebarStartCompact slots — sidebarStart / sidebarStartCompact (React) / app-shell-sidebar-start / app-shell-sidebar-start-compact (Razor)
(default) — neither slot filled
app-shell-sidebar-start — hidden once collapsed
collapsed="true" + app-shell-sidebar-start-compact — the rail-only counterpart
footer slot — footer (React) / app-shell-footer (Razor)
(default) — no footer
app-shell-footer — pinned bar under the content column
nav item — badgeTone, trailing icon, expandable parent — badgeTone / trailingIcon (React) / badge-tone / trailing-icon (Razor)
badge-tone="danger"
badge-tone="success"
badge-tone="warning"
trailing-icon — right-aligned secondary glyph
nested app-nav-link — expandable parent, opens to its active child
edge cases
no app-nav-group — sidebar shows only the brand
app-nav-group with no label — no divider label above its items
long app-shell-topbar-start title — ellipsises, topbar-end stays pinned
many app-nav-link items — nav scrolls independently under a fixed brand/topbar
Attributes
| Attribute | Type | Default | Description |
|---|---|---|---|
| collapsed | bool | false | Start with the sidebar as an icon rail. |
| no-sidebar | bool | false | Full-width, no side nav — a focused wizard or full-bleed report. |
| nav-label | string | Main navigation | Accessible name for the nav element. |
| class | string | — | Extra classes on the shell — merges with the component's own classes (React className). |
<app-nav-group> takes label. <app-nav-link> takes label, icon, href, badge, badge-tone, trailing-icon and active, and may nest <app-nav-link> children to become an expandable parent. The slot elements are app-shell-brand, app-shell-sidebar-start, app-shell-sidebar-start-compact, app-shell-sidebar-footer, app-shell-topbar-start, app-shell-topbar-end, app-shell-content and app-shell-footer.