AppLayout
An application shell: a header, a footer, a left and a right panel, and the content between them. Panels collapse to nothing, to a narrow toolbar rail, or to their full width; they drag to resize between a minimum and maximum; and every region keeps its own scroll region.
Blazor only. For a Visual-Studio-style surface where the user rearranges tabbed, tear-off tool windows, see Docking instead — AppLayout is for a fixed shell whose panels you define.
Features
Section titled “Features”- Three panel states –
Hidden,Toolbar(a narrow rail rendering whatever you put inToolbarContent) andShown, two-way bindable or driven from code. - Drag to resize – a handle on each panel’s inner edge, clamped to
MinSizeandMaxSize, with the final width reported back through@bind-Size. - Independent scroll regions – the panel body, the toolbar rail and the content each scroll on their own while the header, footer and a panel’s pinned header/footer stay put.
- Configurable borders – a default width and colour on the shell that every region inherits and can override, or turn off per region.
- Responsive – under a breakpoint measured on the shell (not the window), an expanded panel compacts to a rail and re-expands as a drawer floating over the content.
- Persistence – opt in with
PersistKeyand a panel’s state and width survive a reload. - Order-independent markup – regions are placed by CSS grid areas, so you can write them in whatever order reads best.
Step 1 — Add the marketplace:
claude plugin marketplace add shinyorg/skillsStep 2 — Install plugins:
claude plugin install shiny-client@shinyclaude plugin install shiny-maui@shinyclaude plugin install controls@shinyclaude plugin install shiny-mediator@shinyclaude plugin install shiny-data@shinyclaude plugin install shiny-aspire@shinyclaude plugin install shiny-extensions@shinyStep 1 — Add the marketplace:
copilot plugin marketplace add https://github.com/shinyorg/skillsStep 2 — Install plugins:
copilot plugin install shiny-client@shinycopilot plugin install shiny-maui@shinycopilot plugin install controls@shinycopilot plugin install shiny-mediator@shinycopilot plugin install shiny-data@shinycopilot plugin install shiny-aspire@shinycopilot plugin install shiny-extensions@shinyGetting started
Section titled “Getting started”Nothing to register — no service, no JS reference. The shell imports its own module.
@using Shiny.Blazor.Controls
<AppLayout Height="100dvh"> <AppLayoutHeader Height="56" Padding="0 16"> <button @onclick="() => nav.ToggleAsync()">☰</button> <strong>My App</strong> </AppLayoutHeader>
<AppLayoutPanel @ref="nav" Side="PanelSide.Left" @bind-State="navState" @bind-Size="navWidth" MinSize="180" MaxSize="420" CollapseBelow="900" PersistKey="nav"> <HeaderContent><h4>Explorer</h4></HeaderContent> <ToolbarContent> <VStack Spacing="4" Align="StackAlign.Center" Padding="8 0"> <button title="Files">🗂</button> <button title="Search">🔍</button> </VStack> </ToolbarContent> <ChildContent> @* this is the scrolling body *@ <NavMenu /> </ChildContent> <FooterContent>…</FooterContent> </AppLayoutPanel>
<AppLayoutContent Padding="20"> @Body </AppLayoutContent>
<AppLayoutFooter Height="36" Padding="0 16">Ready</AppLayoutFooter></AppLayout>
@code { AppLayoutPanel nav = default!; PanelState navState = PanelState.Shown; double navWidth = 260;}AppLayout
Section titled “AppLayout”| Parameter | Type | Default | Description |
|---|---|---|---|
Height |
string |
"100%" |
CSS height of the shell |
HeaderSpan |
LayoutSpan |
Full |
Full runs the header edge to edge; Content insets it between the panels, which then run the full height |
FooterSpan |
LayoutSpan |
Full |
The same for the footer |
BorderWidth |
double |
1 |
Default divider width for every region |
BorderColor |
string? |
theme outline | Default divider colour for every region |
Background |
string? |
null |
CSS background shorthand |
HostWidth |
double |
read-only | Last measured width of the shell |
AppLayoutHeader & AppLayoutFooter
Section titled “AppLayoutHeader & AppLayoutFooter”Height (pixels; null sizes to the content), Border, BorderWidth, BorderColor, Background and Padding. The header draws its divider on the bottom, the footer on the top. Neither scrolls.
AppLayoutContent
Section titled “AppLayoutContent”Scrollable (default true), Background and Padding. There are no border parameters — the header, footer and panels each draw the divider on their own edge.
AppLayoutPanel
Section titled “AppLayoutPanel”| Parameter | Type | Default | Description |
|---|---|---|---|
Side |
PanelSide |
Left |
Which edge the panel docks to |
State |
PanelState |
Shown |
Hidden, Toolbar or Shown. Two-way via @bind-State |
Size |
double |
260 |
Expanded width in pixels. Two-way via @bind-Size, updated after a drag |
MinSize / MaxSize |
double |
140 / 640 |
Drag clamps |
Resizable |
bool |
true |
Show the drag handle on the inner edge |
ToolbarSize |
double |
56 |
Rail width in Toolbar state |
CollapseBelow |
double |
0 |
Shell width under which the panel compacts. Zero disables it |
CollapsedState |
PanelState |
Toolbar |
What an expanded panel collapses to |
PersistKey |
string? |
null |
Saves state and width to localStorage |
Scrollable |
bool |
true |
Give the body its own scroll region |
Border, BorderWidth, BorderColor |
Divider on the edge facing the content | ||
Background |
string? |
null |
CSS background shorthand |
Padding |
string? |
null |
Applied to the body, so a pinned header or footer stays flush |
Content slots
Section titled “Content slots”| Slot | Renders |
|---|---|
HeaderContent |
Pinned above the body |
ChildContent |
The scrolling body |
FooterContent |
Pinned below the body |
ToolbarContent |
Replaces the body in Toolbar state — put anything you like in the rail |
Members
Section titled “Members”| Member | Description |
|---|---|
SetStateAsync(PanelState) |
Move to a state, raising StateChanged |
ToggleAsync() |
Toggle between Shown and CollapsedState |
CurrentState |
The live state, which may differ from State after a collapse |
CurrentSize |
The live width, which may differ from Size after a drag |
IsOverlay |
True while the panel is floating as a compact drawer |
Responsive behaviour
Section titled “Responsive behaviour”CollapseBelow is measured against the shell, not the browser window — an AppLayout nested inside a card compacts when the card gets narrow.
Below the breakpoint an expanded panel drops to CollapsedState. Expanding it while compact floats it over the content as a drawer with a scrim; clicking the scrim dismisses it. Widening back past the breakpoint restores whatever state the panel had before it compacted.
Compacting never writes to PersistKey — it is a response to the viewport, not a preference the user expressed.
Borders
Section titled “Borders”BorderWidth and BorderColor on the shell become the default for every region, resolved through CSS variables so theme tokens still apply. Each region overrides them individually, or removes its divider with Border="false".
<AppLayout BorderWidth="2" BorderColor="var(--shiny-color-primary)"> <AppLayoutHeader Border="false">…</AppLayoutHeader> <AppLayoutPanel Side="PanelSide.Left" BorderColor="#e5e7eb">…</AppLayoutPanel></AppLayout>Accessibility
Section titled “Accessibility”A Hidden panel keeps its element so the width can animate, but is marked inert and aria-hidden — nothing inside it is focusable or announced. The drag handle carries role="separator" with aria-orientation="vertical". The width transition is dropped under prefers-reduced-motion.


