Layout & navigation

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.

Dashboard

Dashboard

Your workspace at a glance — updated a moment ago.

Open invoices
₹4,82,000
8% vs last month
Active clients
128
4 new
Overdue
6
2 since Friday
Reports due
3
today
Recent approvals
Acme Corp — invoice #2041Approved
Globex retainerPending
Initech renewalRejected
Umbrella Ltd — SOWIn review

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.

<app-shell>
    <app-shell-brand><app-icon name="box-seam" size="24" class="text-primary"></app-icon><span class="app-sidebar-brand-text">Acme</span></app-shell-brand>
    <app-nav-group label="Overview">…</app-nav-group>
    <app-shell-topbar-start><h2>Dashboard</h2></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>
collapsed here is a snapshot, not a live toggleEvery playground change re-navigates the page, so the shell renders freshly with that 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.

A complete portal shell
Three nav groups (separated by rules), toned count badges, a trailing lock marker, an expandable Settings parent opened to its active child, a brand, a searchable topbar with a notification action and account mark, the collapse toggle pinned under the nav, and a footer.

Dashboard

Dashboard

Your workspace at a glance — updated a moment ago.

Open invoices
₹4,82,000
8% vs last month
Active clients
128
4 new
Overdue
6
2 since Friday
Reports due
3
today
Recent approvals
Acme Corp — invoice #2041Approved
Globex retainerPending
Initech renewalRejected
Umbrella Ltd — SOWIn review

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.

© 2026 Acme — all rights reserved
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.

Rail state
This shell starts 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.
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.

no-sidebar
The sidebar and its toggle are gone; the content spans the full width. Use it for a checkout flow, an onboarding wizard, or a print-style report.

New invoice

Dashboard

Your workspace at a glance — updated a moment ago.

Open invoices
₹4,82,000
8% vs last month
Active clients
128
4 new
Overdue
6
2 since Friday
Reports due
3
today
Recent approvals
Acme Corp — invoice #2041Approved
Globex retainerPending
Initech renewalRejected
Umbrella Ltd — SOWIn review

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.

Step 2 of 4
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, trailing marker, expandable sub-items
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.

Filling the slots
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-content is optionalAnything left inside <app-shell> that is not captured by a slot becomes the content, so a layout can put @RenderBody() straight inside.
Every class is app--prefixed on purpose.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.

Collapse vs drawer
Above 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>
Preview vs the real thingThe previews above are bounded to a scrollable frame so the docs page can show the shell inline. In a real surface the shell is 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

Dashboard

Page content
collapsed="true"

layout — sidebar vs noSidebar

(default) with sidebar

Dashboard

Page content
no-sidebar="true" — full-width single column

Dashboard

Page content

sidebarStart / sidebarStartCompact slots — sidebarStart / sidebarStartCompact (React) / app-shell-sidebar-start / app-shell-sidebar-start-compact (Razor)

(default) — neither slot filled

Dashboard

Page content
app-shell-sidebar-start — hidden once collapsed

Dashboard

Page content
collapsed="true" + app-shell-sidebar-start-compact — the rail-only counterpart

footer slot — footer (React) / app-shell-footer (Razor)

(default) — no footer

Dashboard

Page content
app-shell-footer — pinned bar under the content column

Dashboard

Page content
© 2026 Acme

nav item — badgeTone, trailing icon, expandable parent — badgeTone / trailingIcon (React) / badge-tone / trailing-icon (Razor)

badge-tone="danger"

Dashboard

Page content
badge-tone="success"

Dashboard

Page content
badge-tone="warning"

Dashboard

Page content
trailing-icon — right-aligned secondary glyph

Dashboard

Page content
nested app-nav-link — expandable parent, opens to its active child

Dashboard

Page content

edge cases

no app-nav-group — sidebar shows only the brand

Dashboard

Page content
app-nav-group with no label — no divider label above its items

Dashboard

Page content
long app-shell-topbar-start title — ellipsises, topbar-end stays pinned

A remarkably long page title that keeps going and going and going

Page content
many app-nav-link items — nav scrolls independently under a fixed brand/topbar

Attributes

AttributeTypeDefaultDescription
collapsedboolfalseStart with the sidebar as an icon rail.
no-sidebarboolfalseFull-width, no side nav — a focused wizard or full-bleed report.
nav-labelstringMain navigationAccessible name for the nav element.
classstring—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.