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, sidebar, shell-bar, main.
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.
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.
Blade Code
Save this as your application's root layout. Two common locations:
resources/views/components/layouts/app.blade.php— the Livewire v4 anonymous-component convention (default in the Livewire Starter Kit). Reference it from a Livewire component class via#[Layout('components.layouts.app')].resources/views/layouts/app.blade.php— the traditional Blade-template path. Reference it from a route or controller via@extends('layouts.app'), or from a Livewire component class via#[Layout('layouts.app')].
Pick whichever matches the scaffolding your fresh Laravel install or starter kit already ships with.
{{-- 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">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">
<x-slot:trigger>
<x-wirekit::profile :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>
<x-wirekit::sidebar.toggle class="lg:hidden" 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 panelled 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 themainlandmark. 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 thelabelyou gave it as its accessible name;mainrenders<main>;shell-barrenders 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 markedactive— 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-labelthat reflects the state it will move to, and the item labels becomesr-onlyrather 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'sstartslot, which is pinned. In the default slot it would scroll out of reach on a phone.