Skip to main content
WireKit
Copy for LLM

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.

Console Shell — rail, module navigation, content

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.

Console Shell — a workspace switcher over nested navigation

Pick a card to continue.

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').
{{-- 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:

{
  "@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:

{{-- 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>
  • App Rail — the module rail, its labeling modes and tones
  • Shell Bar — the aligned column head
  • App Shell — the layout primitive underneath