---
title: Console Shell
description: A module rail, the module's own navigation beside it, and one horizontal rule across all three columns. The shape most modern admin consoles have converged on.
visibility: guest
draft: false
category: application-shells
tags: [application-shells, shell, rail, sidebar, console]
dependencies: [app-rail, app-shell, avatar, badge, button, heading, main, navbar, shell-bar, sidebar, stack, stat, stats, text]
responsive: true
dark_compatible: true
---

# Console Shell

Three columns. A narrow rail of application areas on the outside, the selected area's own
navigation beside it, the content to the right — and one horizontal rule running across all
three at the same height, separating the workspace mark, the module name and the page's own
navigation.

It is the shape an application takes once it has more than one product area. A single sidebar
can answer "where in this area am I"; it cannot also answer "which area am I in" without
becoming a list of lists.

**Composed from:** `app-shell`, `app-rail`, `shell-bar`, `sidebar`, `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.

:::preview{title="Console Shell — rail, module navigation, content" wide bleed}
<x-wirekit::app-shell viewport header-placement="content" style="height: 30rem; overflow: hidden;">
    <x-slot:rail>
        <x-wirekit::app-rail>
            <x-slot:brand>
                <x-wirekit::shell-bar padding="none" align="center">
                    <x-wirekit::avatar initials="SW" size="sm" />
                </x-wirekit::shell-bar>
            </x-slot:brand>
            <x-wirekit::app-rail.item href="#" icon="home" label="Overview" />
            <x-wirekit::app-rail.item href="#" icon="bell" label="Notifications" badge="2" />
            <x-wirekit::app-rail.group :separated="true">
                <x-wirekit::app-rail.item href="#" icon="chart-bar" label="Analytics" />
                <x-wirekit::app-rail.item href="#" icon="credit-card" label="Payments" :active="true" />
                <x-wirekit::app-rail.item href="#" icon="file-text" label="Invoices" />
                <x-wirekit::app-rail.item href="#" icon="shield" label="Risk" />
            </x-wirekit::app-rail.group>
            <x-slot:footer>
                <x-wirekit::app-rail.item href="#" icon="search" label="Search" />
                {{-- The account MENU, opened from the avatar. The name belongs to the mark it
                     stands beside, not to a column two places away — see the prose below. --}}
                <x-wirekit::dropdown placement="right-end">
                    <x-slot:trigger>
                        <x-wirekit::app-rail.item href="#" icon="user" label="Sam Weber" />
                    </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="#">Sign out</x-wirekit::dropdown.item>
                </x-wirekit::dropdown>
            </x-slot:footer>
        </x-wirekit::app-rail>
    </x-slot:rail>
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" label="Payments navigation">
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::text weight="medium">Payments</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:header>
            <x-wirekit::sidebar.item href="#" :active="true">Terminals</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#">Purchases</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#" target="_blank">Settings</x-wirekit::sidebar.item>

        </x-wirekit::sidebar>
    </x-slot:sidebar>
    <x-slot:header>
        <x-wirekit::shell-bar label="Terminal sections">
            <x-slot:start>
                {{-- Below `lg` the rail and the navigation beside it travel off-canvas, so the
                     bar owes them a handle. It sits in `start` rather than in the default slot
                     because that one scrolls: on a phone this bar overflows immediately, and a
                     hamburger that scrolls away is the same as no hamburger. --}}
                <x-wirekit::sidebar.toggle class="lg:hidden" aria-label="Open navigation" />
            </x-slot:start>
            <x-wirekit::navbar.item href="#" :active="true">Transactions</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Terminal info</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Settings</x-wirekit::navbar.item>
            {{-- One action, not two. The trailing cluster keeps its size by contract, so on a
                 phone every pixel it takes comes out of the strip beside it: measured at
                 375px, two buttons left the sections 65px and the reader saw "Transa…". What
                 a bar can afford is a question about the narrowest screen it will meet. --}}
            <x-slot:end>
                <x-wirekit::button size="sm">Create payment</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::heading level="1" size="lg">Terminal A920 Pro 1</x-wirekit::heading>
            <x-wirekit::stats cols="4">
                <x-wirekit::stat label="Status" value="Active" />
                <x-wirekit::stat label="Network" value="Online" />
                <x-wirekit::stat label="Battery" value="72%" />
                <x-wirekit::stat label="Location" value="Berlin, DE" />
            </x-wirekit::stats>
        </x-wirekit::stack>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## A workspace switcher and nested navigation

The second column's head is a `shell-bar` like the others, so anything can go in it — including
a control. A workspace switcher there is the common case: the mark and the name of the thing
you are inside, with a way to change it.

Below it, `<x-wirekit::sidebar.collapsible>` nests a section. Its children carry a guide line
down their inline-start edge, which is what makes the nesting readable at a glance — indentation
alone is ambiguous the moment a label wraps, because the second line of a parent starts at the
same x as a child.

:::preview{title="Console Shell — a workspace switcher over nested navigation" wide bleed}
<x-wirekit::app-shell viewport header-placement="content" style="height: 26rem; overflow: hidden;">
    <x-slot:rail>
        <x-wirekit::app-rail indicator="edge">
            <x-slot:brand>
                <x-wirekit::shell-bar padding="none" align="center">
                    <x-wirekit::avatar initials="SR" size="sm" />
                </x-wirekit::shell-bar>
            </x-slot:brand>
            <x-wirekit::app-rail.item href="#" icon="home" label="Create" :active="true" />
            <x-wirekit::app-rail.item href="#" icon="folders" label="Stock" />
            <x-wirekit::app-rail.item href="#" icon="calendar" label="Calendar" />
            <x-slot:footer>
                <x-wirekit::app-rail.item href="#" icon="settings" label="Settings" />
            </x-slot:footer>
        </x-wirekit::app-rail>
    </x-slot:rail>
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" label="Workspace navigation">
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::dropdown>
                        <x-slot:trigger>
                            <x-wirekit::button size="sm" intent="neutral" surface="ghost">
                                <x-wirekit::avatar initials="S" size="xs" />
                                Southern Rescue
                            </x-wirekit::button>
                        </x-slot:trigger>
                        <x-wirekit::dropdown.item href="#">Southern Rescue</x-wirekit::dropdown.item>
                        <x-wirekit::dropdown.item href="#">Northern Depot</x-wirekit::dropdown.item>
                    </x-wirekit::dropdown>
                </x-wirekit::shell-bar>
            </x-slot:header>
            <x-wirekit::sidebar.item href="#" :active="true">Create</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="#">Stock</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.collapsible label="Teams" icon="users" :open="true">
                <x-wirekit::sidebar.item href="#">Charlie</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#">Bravo</x-wirekit::sidebar.item>
                <x-wirekit::sidebar.item href="#">Delta</x-wirekit::sidebar.item>
            </x-wirekit::sidebar.collapsible>
        </x-wirekit::sidebar>
    </x-slot:sidebar>
    <x-slot:header>
        <x-wirekit::shell-bar label="Card sections">
            <x-slot:start>
                <x-wirekit::sidebar.toggle class="lg:hidden" aria-label="Open navigation" />
            </x-slot:start>
            <x-wirekit::breadcrumb schema="false" :items="[
                ['label' => 'Create', 'href' => '#'],
                ['label' => 'Select card'],
            ]" />
        </x-wirekit::shell-bar>
    </x-slot:header>
    <x-wirekit::main max="none">
        <x-wirekit::text size="sm" variant="muted">Pick a card to continue.</x-wirekit::text>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

A group can also carry its own control. `<x-wirekit::sidebar.group>` takes an `action` slot —
the "add a team" plus beside a heading — and it sits BESIDE the heading rather than inside it,
because an interactive control nested in another one is unreachable by keyboard in practice.

## 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')`.

```blade
{{-- 1. `viewport` pins the shell to the screen so the columns scroll internally
        rather than the page scrolling as one document. --}}
{{-- 2. `header-placement="content"` puts the top bar INSIDE the content column, which
        is what lets both navigation columns run the full height beside it. --}}
<x-wirekit::app-shell viewport header-placement="content">

    {{-- 3. The module rail. Its `brand` slot is the rail's segment of the top rule. --}}
    <x-slot:rail>
        <x-wirekit::app-rail>
            <x-slot:brand>
                <x-wirekit::shell-bar padding="none" align="center">
                    <x-wirekit::avatar :initials="auth()->user()->initials" size="sm" />
                </x-wirekit::shell-bar>
            </x-slot:brand>

            {{-- 4. `label` is not decoration — in this mode nothing of it is drawn, so it
                    is the link's only accessible name and the tooltip's text. --}}
            <x-wirekit::app-rail.item href="{{ route('overview') }}" icon="home" label="Overview" />
            <x-wirekit::app-rail.item href="{{ route('payments') }}" icon="credit-card" label="Payments" />

            {{-- 5. The bottom cluster stays put while the module list scrolls. --}}
            <x-slot:footer>
                <x-wirekit::app-rail.item href="{{ route('profile') }}" icon="user" label="Account" />
            </x-slot:footer>
        </x-wirekit::app-rail>
    </x-slot:rail>

    {{-- 6. The module's own navigation. `variant="flush"` makes it a full-bleed column
            rather than a card, and its `header` slot is the second rule segment. --}}
    <x-slot:sidebar>
        <x-wirekit::sidebar variant="flush" label="Payments navigation">
            <x-slot:header>
                <x-wirekit::shell-bar padding="sm" bleed>
                    <x-wirekit::text weight="medium">Payments</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:header>

            <x-wirekit::sidebar.item href="{{ route('terminals') }}">Terminals</x-wirekit::sidebar.item>
            <x-wirekit::sidebar.item href="{{ route('purchases') }}">Purchases</x-wirekit::sidebar.item>
        </x-wirekit::sidebar>
    </x-slot:sidebar>

    {{-- 7. The third rule segment, in the content column. --}}
    <x-slot:header>
        <x-wirekit::shell-bar label="Terminal sections">
            {{-- The handle for the drawer the two navigation columns become below `lg`.
                    `start` keeps it pinned while the sections beside it scroll. --}}
            <x-slot:start>
                <x-wirekit::sidebar.toggle class="lg:hidden" aria-label="Open navigation" />
            </x-slot:start>
            <x-wirekit::navbar.item href="#" :active="true">Transactions</x-wirekit::navbar.item>
            <x-slot:end>
                <x-wirekit::button size="sm">Create payment</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    {{-- 8. `max="none"` fills the available width. Drop it — or pass a tier like
            `max="xl"` — to box the content instead. --}}
    <x-wirekit::main max="none">
        {{ $slot }}
    </x-wirekit::main>
</x-wirekit::app-shell>
```

## The breadcrumb already carries its structured data

The trail in the content column's head is a `<x-wirekit::breadcrumb>`, and it emits schema.org
`BreadcrumbList` JSON-LD on its own — there is nothing to switch on. It renders nothing visible,
which is why it is easy to assume it is missing:

```json
{
  "@context": "https://schema.org",
  "@type": "BreadcrumbList",
  "itemListElement": [
    { "@type": "ListItem", "position": 1, "name": "Create", "item": "/create" },
    { "@type": "ListItem", "position": 2, "name": "Select card" }
  ]
}
```

Note the last entry: it is named but carries no `item`. That is the current page, and Google's
guidance is that it is included in the trail without a URL — a final entry that links somewhere
reads as pointing away from the page it describes.

One caveat for this exact layout. A console usually has room for a breadcrumb in the top bar
**and** a heading in the content, and it is tempting to put a second trail in the body. Pass
`schema="false"` on that one: a page must carry exactly one `BreadcrumbList`, and two of them
compete rather than combine.

## Where the account goes

The signed-in person's name belongs to the mark that represents them, not to a column beside
it. In the rail that means the avatar is the trigger and the name is what the menu it opens
carries — so clicking the one thing that looks like "you" produces everything about you, and
the navigation column stays a list of destinations.

Wrap the footer's account entry in a `<x-wirekit::dropdown>` and pass the person's name as the
item's `label`: it is the accessible name of the trigger either way, and in a wide rail it is
also what is drawn.

## The rule that runs across the columns

Each column's head is a `<x-wirekit::shell-bar>`. That is the whole mechanism: every bar reads
`--size-wk-shell-bar` for its height, so the three segments cannot drift apart the way three
hand-aligned headers would the first time one column gets a longer title.

The three bars use different `padding` values on purpose. The rail's is `none` — its content
is a centered mark, not text on a spine. The second column's is `sm`, matching the narrower
tier its own items use. The content column's is the default `lg`, the page-edge spine, so a
title in the bar lines up with the first paragraph below it.

## Full width or boxed

`<x-wirekit::main>` boxes its content to a container width by default, which is right for a
document and wrong for a console — a transaction table wants the screen. `max="none"` fills
the available width; any other tier boxes it.

## Below the breakpoint

The rail and the module navigation travel as ONE off-canvas panel rather than sliding
independently — two panels moving at different offsets land as a seam. The shell owns the flag,
so any control inside it can open the drawer:

```blade
{{-- 1. `sidebarOpen` lives on the shell root, so a trigger anywhere inside it can flip it. --}}
{{-- 2. `lg:hidden` because above the breakpoint both columns are in flow and there is
        nothing to open. --}}
<x-wirekit::button x-on:click="sidebarOpen = true" class="lg:hidden">
    Menu
</x-wirekit::button>
```

## Related

- [App Rail](/components/app-rail) — the module rail, its labeling modes and tones
- [Shell Bar](/components/shell-bar) — the aligned column head
- [App Shell](/components/app-shell) — the layout primitive underneath
