---
title: Sidebar Shell
description: One navigation column at full height, the top bar inside the content column beside it — the shape most admin panels and dashboards take.
visibility: guest
draft: false
category: application-shells
tags: [application-shells, shell, sidebar, flush, collapsible]
dependencies: [app-shell, avatar, button, card, dropdown, footer, heading, main, profile, shell-bar, sidebar, skip-link, stack, stat, stats, text]
responsive: true
dark_compatible: true
---

# Sidebar Shell

One navigation column running the full height of the window, the content beside it, and the
top bar inside that content column rather than spanning the whole width. It is the shape most
admin panels and dashboards take, and the reason is the column: everything the application
can do is visible at once, in one list, without a second level of chrome.

Leave it the moment the application has several product areas. A single column then has to
answer two questions at the same time — which area am I in, and where am I inside it — and it
answers both badly. That is where the console shape starts, with a rail for the areas and a
column for the pages inside one.

**Composed from:** `app-shell`, `avatar`, `button`, `card`, `dropdown`, `footer`, `heading`, `main`, `profile`, `shell-bar`, `sidebar`, `skip-link`, `stack`, `stat`, `stats`, `text`.

> **A shell has no outer edge of its own.** It fills the browser window, so in a real
> application its outer edges ARE the window's edges — square, and as wide as the screen.
> Every radius in this family faces INWARD, toward the inset content panel; nothing faces out.
> The previews below carry no frame for the same reason: the box they sit in is standing in
> for the window, and a shell drawn inside a second border is a window inside a window.

:::preview{title="Sidebar Shell — one navigation column, the bar inside the content" wide bleed height="28rem"}
<x-wirekit::app-shell viewport header-placement="content" :sidebar-inset="false">
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" label="Main navigation">
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::avatar initials="AC" size="sm" />
                    <x-wirekit::text weight="semibold" class="wk-rail-hide">Acme Console</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:header>

            <x-wirekit::sidebar.group label="Overview">
                <x-wirekit::sidebar.item href="#" icon="dashboard" active>Dashboard</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#" icon="users">Customers</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#" icon="file-text">Invoices</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#" icon="tag">Products</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            <x-wirekit::sidebar.group label="Workspace">
                <x-wirekit::sidebar.item href="#" icon="settings">Settings</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            <x-slot:footer>
                <x-wirekit::shell-bar padding="xs" bleed rule="top">
                <x-wirekit::dropdown placement="top-start" block>
                    <x-slot:trigger>
                        <x-wirekit::profile radius="nav-item" padding="sm" tone="muted" name="Dana Ortiz" :avatar="['initials' => 'DO']" interactive />
                    </x-slot:trigger>
                    <x-wirekit::dropdown.item href="#">Profile</x-wirekit::dropdown.item>
                    <x-wirekit::dropdown.item href="#">Preferences</x-wirekit::dropdown.item>
                    <x-wirekit::dropdown.separator />
                    <x-wirekit::dropdown.item href="#" danger>Sign out</x-wirekit::dropdown.item>
                </x-wirekit::dropdown>
                </x-wirekit::shell-bar>
            </x-slot:footer>
        </x-wirekit::sidebar>
    </x-slot:sidebar>

    <x-slot:header>
        <x-wirekit::shell-bar>
            {{-- The toggle sits in `start`, not the default slot: the default slot SCROLLS
                 when the bar overflows, and on a phone it overflows immediately — a burger
                 that scrolls away is the one control a reader cannot afford to lose. --}}
            <x-slot:start class="lg:hidden">
                <x-wirekit::sidebar.toggle aria-label="Open navigation" />
            </x-slot:start>

            <x-wirekit::heading level="1" size="md">Dashboard</x-wirekit::heading>

            <x-slot:end>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm">Export</x-wirekit::button>
                <x-wirekit::button size="sm">New invoice</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main max="none">
        <x-wirekit::stack gap="lg">
            <x-wirekit::stats cols="4">
                <x-wirekit::stat label="Revenue" value="$48,120" change="+12.4%" trend="up" />
                <x-wirekit::stat label="Open invoices" value="37" change="-4.1%" trend="down" />
                <x-wirekit::stat label="Customers" value="1,284" change="+3.0%" trend="up" />
                <x-wirekit::stat label="Churn" value="1.8%" change="-0.3%" trend="up" />
            </x-wirekit::stats>

            <x-wirekit::text intent="muted">Figures update hourly. The column on the left stays put while this area scrolls.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Collapsing the column to icons

A column that is always open costs the same width on every page, and on a laptop that width
is real estate the content wanted. `collapsible` gives the reader a handle to give it back:
the labels go, the icons stay, and the column narrows to the width of one icon plus its
padding.

The labels are not removed from the page when this happens — they become `sr-only`, so a
screen reader still hears "Customers" where a sighted reader now sees only the icon. That is
the whole reason the item takes a label at all rather than an icon with a tooltip.

`persist` remembers the reader's choice across page loads by writing it to the browser's own
storage, which is what you want in a real application. It is deliberately absent from the
preview below: a preview whose rendered state depends on the visitor's storage shows
different things to different people, and one of them is always the wrong one to document.

:::preview{title="The same column, collapsed to icons" wide bleed height="24rem"}
<x-wirekit::app-shell viewport header-placement="content" :sidebar-inset="false">
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" collapsible collapsed label="Main navigation">
            <x-wirekit::sidebar.item href="#" icon="dashboard" active>Dashboard</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#" icon="users">Customers</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#" icon="file-text">Invoices</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#" icon="settings">Settings</x-wirekit::sidebar.item>
        </x-wirekit::sidebar>
    </x-slot:sidebar>

    <x-slot:header>
        <x-wirekit::shell-bar>
            <x-slot:start class="lg:hidden">
                <x-wirekit::sidebar.toggle aria-label="Open navigation" />
            </x-slot:start>

            <x-wirekit::heading level="1" size="md">Dashboard</x-wirekit::heading>
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main max="none">
        <x-wirekit::text intent="muted">The column keeps its icons and gives the labels back to the content.</x-wirekit::text>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Everything at once

The two previews above each isolate one decision, which is what makes them readable and what
makes them incomplete: neither shows a shell with all of its parts in place at the same time.
This one does, and it is the one to copy from. Every zone the shell has is filled — a brand
row above the navigation, labeled groups, a nested collapsible section, an item carrying an
unread count, the collapse control, an account row pinned to the foot of the column, a bar
with both of its clusters, and a real page footer at the end of the content.

::: tip Start here and strip back
Furnishing an empty shell means working out which slot each part belongs in. Starting from a
full one means taking out the slots you have no content for, and what is left still stands up —
the zones are independent of one another.
:::

:::preview{title="Sidebar Shell — every zone filled" wide bleed height="34rem"}
<x-wirekit::app-shell viewport header-placement="content" :sidebar-inset="false">
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" collapsible persist="wk-docs-sidebar-shell-full" label="Main navigation">
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::avatar initials="AC" size="sm" />
                    <x-wirekit::text weight="semibold" class="wk-rail-hide">Acme Console</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:header>

            <x-wirekit::sidebar.group label="Overview">
                <x-wirekit::sidebar.item href="#" icon="dashboard" active>Dashboard</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#" icon="inbox" badge="12">Inbox</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#" icon="users">Customers</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            <x-wirekit::sidebar.group label="Billing">
                <x-wirekit::sidebar.item href="#" icon="file-text">Invoices</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.collapsible label="Reports" icon="chart-bar" :open="true">
                    <x-wirekit::sidebar.item href="#" icon="chart-bar">Revenue</x-wirekit::sidebar.item>
                    <x-wirekit::sidebar.item href="#" icon="send">Churn</x-wirekit::sidebar.item>
                    <x-wirekit::sidebar.item href="#" icon="file-text">Forecast</x-wirekit::sidebar.item>
                </x-wirekit::sidebar.collapsible>
            </x-wirekit::sidebar.group>

            <x-wirekit::sidebar.group label="Workspace">
                <x-wirekit::sidebar.item href="#" icon="settings">Settings</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            <x-slot:footer>
                <x-wirekit::shell-bar padding="xs" bleed rule="top">
                    <x-wirekit::dropdown placement="top-start" block>
                        <x-slot:trigger>
                            <x-wirekit::profile radius="nav-item" padding="sm" tone="muted" name="Dana Ortiz" :avatar="['initials' => 'DO']" interactive />
                        </x-slot:trigger>
                        <x-wirekit::dropdown.item href="#">Profile</x-wirekit::dropdown.item>
                        <x-wirekit::dropdown.item href="#">Preferences</x-wirekit::dropdown.item>
                        <x-wirekit::dropdown.separator />
                        <x-wirekit::dropdown.item href="#" danger>Sign out</x-wirekit::dropdown.item>
                    </x-wirekit::dropdown>
                </x-wirekit::shell-bar>
            </x-slot:footer>
        </x-wirekit::sidebar>
    </x-slot:sidebar>

    <x-slot:header>
        <x-wirekit::shell-bar>
            <x-slot:start class="lg:hidden">
                <x-wirekit::sidebar.toggle aria-label="Open navigation" />
            </x-slot:start>

            <x-wirekit::heading level="1" size="md">Dashboard</x-wirekit::heading>

            <x-slot:end>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm">Export</x-wirekit::button>
                <x-wirekit::button size="sm">New invoice</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main max="none">
        <x-wirekit::stack gap="lg">
            <x-wirekit::stats cols="4">
                <x-wirekit::stat label="Revenue" value="$48,120" change="+12.4%" trend="up" />
                <x-wirekit::stat label="Open invoices" value="37" change="-4.1%" trend="down" />
                <x-wirekit::stat label="Customers" value="1,284" change="+3.0%" trend="up" />
                <x-wirekit::stat label="Churn" value="1.8%" change="-0.3%" trend="up" />
            </x-wirekit::stats>

            <x-wirekit::card>
                <x-wirekit::card.body>
                    <x-wirekit::stack gap="sm">
                        <x-wirekit::heading level="2" size="sm">This week</x-wirekit::heading>
                        <x-wirekit::text intent="muted">Nine invoices went out and four were settled. The column on the left stays put while this area scrolls.</x-wirekit::text>
                    </x-wirekit::stack>
                </x-wirekit::card.body>
            </x-wirekit::card>
        </x-wirekit::stack>

        <x-wirekit::footer max="full">
            <x-wirekit::text size="sm" intent="muted">&copy; 2026 Acme Inc.</x-wirekit::text>
        </x-wirekit::footer>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Blade Code

Save this as your application's root layout. Two common locations:

- `resources/views/layouts/app.blade.php` — Livewire v4's default page layout (`layouts::app`) and the path `php artisan livewire:layout` creates. Livewire uses it without an attribute; reference it explicitly from a Livewire component class via `#[Layout('layouts.app')]`, or from a route or controller view via `@extends('layouts.app')`.
- `resources/views/components/layouts/app.blade.php` — the anonymous-component path Livewire v3 used by default, still found in projects created before v4. Reference it via `#[Layout('components.layouts.app')]`.

In the Livewire Starter Kit, `layouts/app.blade.php` only hands the page on to a shell in `layouts/app/` — that shell is the file holding the `<head>` and `<body>`.

Pick whichever matches the scaffolding your fresh Laravel install or starter kit already ships with.

```blade
{{-- 1. The skip link sits OUTSIDE the shell and before it. Under
     header-placement="content" the navigation column comes first in DOM order, so a skip
     link placed inside would be reached only after every navigation item — which is the
     one thing it exists to prevent. --}}
<x-wirekit::skip-link />

{{-- 2. `viewport` makes the shell fill the window rather than flow with the page. It is
     what turns this from a layout into a shell. --}}
<x-wirekit::app-shell viewport header-placement="content" :sidebar-inset="false">

    {{-- 3. The navigation column. `variant="flush"` draws no surface of its own, so the
         column sits directly on the shell's background instead of being a panel inside it. --}}
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" collapsible persist="app.sidebar" label="Main navigation">

            {{-- 4. The column's own head. `bleed` lets the bar reach the column's edges, so
                 its bottom edge lines up with the bar in the content column beside it. --}}
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::avatar initials="AC" size="sm" />
                    <x-wirekit::text weight="semibold" class="wk-rail-hide">Acme Console</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:header>

            {{-- 5. `icon` takes a VOCABULARY name, not a glyph name — so the same markup
                 keeps its meaning if the application changes icon set. --}}
            <x-wirekit::sidebar.group label="Overview">
                <x-wirekit::sidebar.item href="{{ route('dashboard') }}" icon="dashboard" :active="request()->routeIs('dashboard')">Dashboard</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="{{ route('customers.index') }}" icon="users" :active="request()->routeIs('customers.*')">Customers</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="{{ route('invoices.index') }}" icon="file-text" :active="request()->routeIs('invoices.*')">Invoices</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            <x-wirekit::sidebar.group label="Workspace">
                <x-wirekit::sidebar.item href="{{ route('settings') }}" icon="settings" :active="request()->routeIs('settings')">Settings</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.group>

            {{-- 6. The account menu goes in the column's footer zone. Nothing needs to be
                 pushed down by hand — the zone is already anchored to the bottom. --}}
            <x-slot:footer>
                <x-wirekit::dropdown placement="top-start" block>
                    <x-slot:trigger>
                        <x-wirekit::profile radius="nav-item" padding="sm" tone="muted" :name="auth()->user()->name" :avatar="['initials' => auth()->user()->initials()]" interactive />
                    </x-slot:trigger>
                    <x-wirekit::dropdown.item href="{{ route('profile') }}">Profile</x-wirekit::dropdown.item>
                    <x-wirekit::dropdown.item href="{{ route('preferences') }}">Preferences</x-wirekit::dropdown.item>
                    <x-wirekit::dropdown.separator />
                    <x-wirekit::dropdown.item href="{{ route('logout') }}" danger>Sign out</x-wirekit::dropdown.item>
                </x-wirekit::dropdown>
            </x-slot:footer>
        </x-wirekit::sidebar>
    </x-slot:sidebar>

    {{-- 7. The toggle belongs in the bar's `start` slot, which is pinned. The default slot
         scrolls when the bar overflows, and on a phone it overflows immediately — so a
         toggle placed there scrolls out of reach exactly when it is needed. --}}
    <x-slot:header>
        <x-wirekit::shell-bar>
            <x-slot:start class="lg:hidden">
                <x-wirekit::sidebar.toggle aria-label="Open navigation" />
            </x-slot:start>

            <x-wirekit::heading level="1" size="md">{{ $title ?? 'Dashboard' }}</x-wirekit::heading>

            <x-slot:end>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm" href="{{ route('exports') }}">Export</x-wirekit::button>
                <x-wirekit::button size="sm" href="{{ route('invoices.create') }}">New invoice</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    {{-- 8. `id="main-content"` is what the skip link in step 1 jumps to, and setting it also
         gives the landmark tabindex="-1" so the jump moves FOCUS and not just the scroll
         position. `max="none"` lets the content use the full column width. --}}
    <x-wirekit::main max="none" id="main-content">
        {{ $slot }}
    </x-wirekit::main>
</x-wirekit::app-shell>
```

## Customization

| What | How |
|------|-----|
| Column width | Set `--wk-sidebar-w` on the shell or any ancestor — the sidebar reads it and falls back to `16rem`. It is a CSS variable rather than a prop so a whole application can set it once. |
| Collapsed width | Fixed at the width of one icon plus its padding. It is not themeable on purpose: the collapsed column exists to be exactly as wide as its icons, and a different value makes it a narrow column instead of an icon rail. |
| Column surface | `variant="flush"` above draws no surface of its own, which is what a column against the shell's own background wants. Drop it for a paneled column with its own border and elevation. |
| Bar height and padding | `padding` on `shell-bar` (`sm`, default, `lg`). The bar's bottom edge is the shell's horizontal rule, so it aligns with the same edge in the other columns automatically. |
| Navigation items | `sidebar.group` for the headings, `sidebar.item` for the rows. The `icon` prop takes a vocabulary name, not a glyph name, so it survives a change of icon set. |
| Account menu | The `footer` slot of the sidebar. Anything can go there; the composition above uses a `dropdown` because a menu of account actions is what most applications put there. |

## Accessibility

Everything below is what the components emit — there is nothing to wire up by hand.

- **Skip link** — `<x-wirekit::skip-link />` at the top of the page, visible on keyboard focus,
  targeting the `main` landmark. It is the first thing a keyboard reader meets, and it exists
  because the alternative is tabbing through the whole navigation column on every page.
- **Landmarks** — the sidebar renders `<nav>` with the `label` you gave it as its accessible
  name; `main` renders `<main>`; `shell-bar` renders a `<header>` inside the content column.
  Three landmarks, each named, which is what lets a screen reader jump between them.
- **`aria-current="page"`** on the item marked `active` — the item is a link to a page, and
  this says which page you are on. It is emitted for you; do not add it by hand.
- **The collapse toggle** carries an `aria-label` that reflects the state it will move to,
  and the item labels become `sr-only` rather than being removed, so the column keeps its
  full meaning for a screen reader when it is visually reduced to icons.
- **The mobile toggle** carries `aria-label="Open navigation"` and sits in the bar's `start`
  slot, which is pinned. In the default slot it would scroll out of reach on a phone.

## Production Considerations

- **The collapsed state is the reader's, and it belongs to their browser.** `persist` keeps it
  across navigations on one device; if it should follow the account, store it server-side and
  pass the value in rather than reaching for the prop.
- **The active item comes from the server.** Resolving it in the browser leaves the first paint
  with nothing marked and the highlight arriving late on every navigation — visible on a slow
  connection, and invisible in development.
- **A navigation column is rendered on every page**, so anything expensive in it is expensive
  everywhere. Counts and badges in the sidebar are the classic case: cache them, or accept that
  every page in the application now waits for them.
- **Below the breakpoint the column becomes a drawer**, and a drawer over content needs the
  background inert while it is open — otherwise a keyboard reader tabs into a page they cannot
  see. The component handles it; a hand-rolled replacement has to.

## Related

- [App Shell](/components/app-shell) — the primitive this page composes
- [Sidebar](/components/sidebar) — the navigation column itself
- [Shell Bar](/components/shell-bar) — the top bar inside the content column
