---
title: Rail Shell
description: A single module rail against an inset content panel — the console shape for an application whose areas need no second navigation column.
visibility: guest
draft: false
category: application-shells
tags: [application-shells, shell, rail, panel, tone]
dependencies: [app-rail, app-shell, avatar, badge, button, heading, main, shell-bar, stack, text]
responsive: true
dark_compatible: true
---

# Rail Shell

The console shape without a second navigation column: a module rail on the outside, and the
content directly beside it as an inset panel. Use it when each area is one screen — a board, a
queue, an editor — and a second list of destinations would be a list with one entry.

It is also where the shell's two color decisions become visible. The chrome carries a tone, the
content panel does not, and the radius between them is what makes the panel read as a sheet
lying on the chrome rather than as a fourth border.

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

:::preview{title="Rail Shell — dark chrome, inset content panel" wide bleed}
<x-wirekit::app-shell viewport panel tone="inverse" header-placement="content" style="height: 28rem; 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="AI" size="sm" />
                </x-wirekit::shell-bar>
            </x-slot:brand>
            <x-wirekit::app-rail.item href="#" icon="chart-bar" label="Insights" />
            <x-wirekit::app-rail.item href="#" icon="file-text" label="Invoicing" :active="true" />
            <x-wirekit::app-rail.item href="#" icon="inbox" label="Inbox" badge="5" />
            <x-wirekit::app-rail.item href="#" icon="users" label="Suppliers" />
            <x-wirekit::app-rail.item href="#" icon="tag" label="Labels" />
            <x-slot:footer>
                <x-wirekit::app-rail.item href="#" icon="bell" label="Alerts" />
                <x-wirekit::app-rail.item href="#" icon="settings" label="Settings" />
                <x-wirekit::app-rail.item href="#" icon="user" label="Account" />
            </x-slot:footer>
        </x-wirekit::app-rail>
    </x-slot:rail>
    <x-slot:header>
        <x-wirekit::shell-bar>
            <x-slot:start>
                {{-- Below `lg` the rail travels off-canvas, so the bar owes it a handle.
                     `start` keeps the handle pinned while the cluster beside it scrolls —
                     put it in the default slot and it scrolls away with the title. --}}
                <x-wirekit::sidebar.toggle class="lg:hidden" aria-label="Open navigation" />
            </x-slot:start>
            <x-wirekit::heading level="1" size="md">AI Invoicing</x-wirekit::heading>
            <x-slot:end>
                <x-wirekit::button size="sm" intent="neutral" surface="outline">Filters</x-wirekit::button>
                <x-wirekit::button size="sm">Upload invoice</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>
    <x-wirekit::main max="none">
        <x-wirekit::stack gap="md">
            <x-wirekit::text size="sm" variant="muted">128 documents, 24 awaiting review.</x-wirekit::text>
            <x-wirekit::card>
                <x-wirekit::card.body>
                    <x-wirekit::stack gap="sm">
                        <x-wirekit::row gap="sm">
                            <x-wirekit::badge intent="warning">Manual review</x-wirekit::badge>
                            <x-wirekit::text size="sm" variant="muted">Dec 30, 2025</x-wirekit::text>
                        </x-wirekit::row>
                        <x-wirekit::text weight="medium">Ford Civil Contracting Pty Ltd.</x-wirekit::text>
                        <x-wirekit::text size="sm" variant="muted">PO-1048 · Job-001</x-wirekit::text>
                    </x-wirekit::stack>
                </x-wirekit::card.body>
            </x-wirekit::card>
        </x-wirekit::stack>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Blade Code

```blade
{{-- 1. `tone="inverse"` puts the shell's chrome on the dark surface; `panel` lifts the
        content column off it as an inset, rounded sheet. --}}
{{-- 2. The rail inherits the shell's tone — it needs no `tone` of its own, and giving it
        one is how you make it differ from the chrome deliberately. --}}
<x-wirekit::app-shell viewport panel tone="inverse" header-placement="content">
    <x-slot:rail>
        {{-- 3. `indicator="edge"` marks the current module with a bar on the rail's
                inline-end edge instead of filling its box — which reads better in a very
                narrow column, where a filled box dominates everything beside it. --}}
        <x-wirekit::app-rail indicator="edge">
            <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>

            <x-wirekit::app-rail.item href="{{ route('invoicing') }}" icon="file-text" label="Invoicing" />
            <x-wirekit::app-rail.item href="{{ route('inbox') }}" icon="inbox" label="Inbox" />

            <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>

    {{-- 4. With no `sidebar` slot the content column begins directly after the rail. --}}
    <x-slot:header>
        <x-wirekit::shell-bar>
            {{-- 5. The handle for the drawer the rail becomes below `lg`. Without it the
                    rail is off-canvas on a phone with nothing to open it. --}}
            <x-slot:start>
                <x-wirekit::sidebar.toggle class="lg:hidden" aria-label="Open navigation" />
            </x-slot:start>
            <x-wirekit::heading level="1" size="md">AI Invoicing</x-wirekit::heading>
            <x-slot:end>
                <x-wirekit::button size="sm">Upload invoice</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main max="none">
        {{ $slot }}
    </x-wirekit::main>
</x-wirekit::app-shell>
```

## Letting the rail show its names

An application with one navigation level often wants the rail wide enough to read. Add
`expandable` and the reader chooses; `persist` remembers the choice.

:::preview{title="Expandable, with the names visible" wide bleed}
<x-wirekit::app-shell viewport panel tone="muted" header-placement="content" style="height: 24rem; overflow: hidden;">
    <x-slot:rail>
        <x-wirekit::app-rail expandable expanded persist="wk-docs-rail-shell">
            <x-slot:brand>
                <x-wirekit::shell-bar padding="none">
                    <x-wirekit::avatar initials="AD" size="sm" />
                    <x-wirekit::text size="sm" weight="medium" data-wk-rail-brand-label>Acme Design</x-wirekit::text>
                </x-wirekit::shell-bar>
            </x-slot:brand>
            <x-wirekit::app-rail.item href="#" icon="home" label="Home" :active="true" />
            <x-wirekit::app-rail.item href="#" icon="folders" label="Ads library" />
            <x-wirekit::app-rail.item href="#" icon="file-text" label="Briefs" />
            <x-wirekit::app-rail.item href="#" icon="chart-bar" label="Reports" badge="4" />
            <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: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">Home</x-wirekit::heading>
        </x-wirekit::shell-bar>
    </x-slot:header>
    <x-wirekit::main max="none">
        <x-wirekit::text size="sm" variant="muted">Collapse the rail with the control above the module list. The choice survives a reload.</x-wirekit::text>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Choosing the two colors

The chrome and the content are separate decisions, and both are token-level.

| Want | Set |
| --- | --- |
| Dark chrome, light content | `tone="inverse"` on the shell, plus `panel` |
| A quiet step down from the page | `tone="muted"` |
| Brand-colored chrome | `tone="accent"` |
| A rail that differs from the rest of the chrome | a `tone` on the rail as well as the shell |
| A different radius for the sheet | `--radius-wk-shell-panel` |
| Different colors entirely | the `--color-wk-rail-*` roles |

The panel's radius is its own token rather than the generic large radius, because the sheet is
the biggest rounded surface on the page: a theme that wants sharp cards with a soft shell — or
the reverse — has no way to say so if the two share one value.

**The inset is even on all four sides**, and that is what makes it read as a sheet. It used to
round only the corners facing the chrome and sit flush against it — which opens a wedge of
chrome beside the rail's straight edge and reads, correctly, as a gap rather than as a panel. A
curve needs something to curve away from. It uses the same token the rail's own `panel` variant
insets by, so a floating rail and a floating content panel sit on one grid.

A navigation column standing beside an inset panel **draws no edge of its own**: the gap is the
separation, and its border would be a line floating in the middle of the chrome.

Below the breakpoint the inset is dropped, and the edges come back with it. The navigation is a
drawer there and the content is the whole viewport, so an inset would spend horizontal room a
phone does not have on a decoration nothing is beside.

### Would a real browser round the outer corners?

No, and it does not. Every rounded corner in this family faces INWARD — the content panel's
corners face the chrome it lies on. The shell's own outer edge carries no radius at all, so in a
real full-viewport application it meets the browser's sharp edge squarely, which is the only
thing that looks right there.

The rounded outline around these examples is the documentation's **preview frame**, not the
component.

## Related

- [Console Shell](/layouts/application-shells/console-shell) — the same rail with a second navigation column
- [App Rail](/components/app-rail) — labeling modes, tones and the expand toggle
- [Theming](/theming) — the token roles behind the tones
