---
title: Stacked Shell
description: One bar across the top, the content below it, and no navigation column at all — the shape for an application whose sections fit in a single row.
visibility: guest
draft: false
category: application-shells
tags: [application-shells, shell, stacked, navbar, breadcrumb]
dependencies: [app-shell, avatar, breadcrumb, button, card, dropdown, footer, heading, icon, main, navbar, shell-bar, skip-link, stack, stat, stats, text]
responsive: true
dark_compatible: true
---

# Stacked Shell

One bar across the top, the content below it, and no navigation column at all. Everything the
application can do fits in a single row, which is the condition for this shape and also its
limit: a row holds about seven items before it starts hiding things behind a menu, and a menu
of sections is a column that has been folded up badly.

The bar spans the full width, and the content region below it is the only thing that scrolls.
That is what separates the shape from an ordinary page with a header — a page scrolls the bar
away with everything else.

**Composed from:** `app-shell`, `avatar`, `breadcrumb`, `button`, `card`, `dropdown`, `footer`, `heading`, `icon`, `main`, `navbar`, `shell-bar`, `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.

When the sections stop fitting in a row, the answer is a column: see
[Sidebar Shell](/blueprints/application-shells/sidebar-shell), or
[Console Shell](/blueprints/application-shells/console-shell) once there are several product
areas rather than several pages.

:::preview{title="Stacked Shell — one bar across the top, the trail under it" wide bleed height="26rem"}
<x-wirekit::app-shell viewport>
    <x-slot:header>
        <x-wirekit::navbar>
            <x-slot:brand>
                <x-wirekit::avatar initials="AC" size="sm" />
                <x-wirekit::text weight="semibold">Acme</x-wirekit::text>
            </x-slot:brand>

            <x-wirekit::navbar.item href="#" active>Overview</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Projects</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Reports</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Team</x-wirekit::navbar.item>

            <x-slot:actions>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Notifications">
                    <x-wirekit::icon name="bell" size="sm" />
                </x-wirekit::button>

                <x-wirekit::dropdown placement="bottom-end">
                    <x-slot:trigger>
                        {{-- A real button, because the dropdown puts its ARIA on the inner
                             interactive element and an avatar is not one. --}}
                        <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Account menu">
                            <x-wirekit::avatar initials="DO" size="sm" />
                        </x-wirekit::button>
                    </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-slot:actions>
        </x-wirekit::navbar>

        <x-wirekit::shell-bar label="Page location">
            {{-- `schema="false"` because a preview is a fragment on a page that already has
                 its own structured data; a second trail would claim the page is somewhere it
                 is not. The copy-paste fence below deliberately omits it. --}}
            <x-wirekit::breadcrumb schema="false" :items="[
                ['label' => 'Projects', 'href' => '#'],
                ['label' => 'Atlas', 'href' => '#'],
                ['label' => 'Milestones'],
            ]" />
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main>
        <x-wirekit::stack gap="md">
            <x-wirekit::heading level="1" size="lg">Milestones</x-wirekit::heading>
            <x-wirekit::text intent="muted">The bar stays where it is. Only this region scrolls, which is what makes the shape a shell rather than a page with a header on top.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## 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. Outside the shell and before it, so a keyboard reader meets it first. --}}
<x-wirekit::skip-link />

{{-- 2. `viewport` fills the window. Without it this is a page with a header, and the bar
     scrolls away with the content. --}}
<x-wirekit::app-shell viewport>

    {{-- 3. Two children in one header slot: the navigation bar, and the trail under it.
         They stack, and the region below them takes the remaining height. --}}
    <x-slot:header>
        <x-wirekit::navbar>
            <x-slot:brand>
                <x-wirekit::avatar initials="AC" size="sm" />
                <x-wirekit::text weight="semibold">Acme</x-wirekit::text>
            </x-slot:brand>

            {{-- 4. `:active` is what emits aria-current="page". It is the only thing that
                 tells a screen reader which section it is in. --}}
            <x-wirekit::navbar.item href="{{ route('overview') }}" :active="request()->routeIs('overview')">Overview</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="{{ route('projects.index') }}" :active="request()->routeIs('projects.*')">Projects</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="{{ route('reports.index') }}" :active="request()->routeIs('reports.*')">Reports</x-wirekit::navbar.item>

            <x-slot:actions>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Notifications">
                    <x-wirekit::icon name="bell" size="sm" />
                </x-wirekit::button>

                {{-- 5. The trigger is a button wrapping the avatar, not the avatar itself:
                     the dropdown puts its ARIA on the inner interactive element. --}}
                <x-wirekit::dropdown placement="bottom-end">
                    <x-slot:trigger>
                        <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Account menu">
                            <x-wirekit::avatar :initials="auth()->user()->initials()" size="sm" />
                        </x-wirekit::button>
                    </x-slot:trigger>
                    <x-wirekit::dropdown.item href="{{ route('profile') }}">Profile</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:actions>
        </x-wirekit::navbar>

        {{-- 6. The trail gets its own labeled bar, so it is a named landmark rather than
             loose text under the navigation. --}}
        <x-wirekit::shell-bar label="Page location">
            <x-wirekit::breadcrumb :items="$breadcrumbs ?? []" />
        </x-wirekit::shell-bar>
    </x-slot:header>

    {{-- 7. `id="main-content"` is the skip link's target, and setting it also gives the
         landmark tabindex="-1" so the jump moves focus rather than only scrolling. --}}
    <x-wirekit::main id="main-content">
        {{ $slot }}
    </x-wirekit::main>
</x-wirekit::app-shell>
```

## Passing the trail

The bar is the same on every page; the trail is not. Hand it in from the page rather than
building it inside the layout, so a page that has no trail simply passes nothing.

```blade
{{-- resources/views/livewire/projects/milestones.blade.php --}}
<x-layouts.app :breadcrumbs="[
    ['label' => 'Projects', 'href' => route('projects.index')],
    ['label' => $project->name, 'href' => route('projects.show', $project)],
    ['label' => 'Milestones'],
]">
    <x-wirekit::stack gap="md">
        <x-wirekit::heading level="1" size="lg">Milestones</x-wirekit::heading>
        {{ $milestones->links() }}
    </x-wirekit::stack>
</x-layouts.app>
```

## Giving the content a width

`main` boxes its content at a readable measure by default and centers it under the full-width
bar. That pairing is the whole reason the shape survives on a wide monitor: the bar's rule
keeps the window's width because it is chrome, and the text does not, because nobody reads a
line that is 2000 pixels long.

**The bar's RULE and the bar's ITEMS are two different questions**, and the preview below
answers both. The rule runs edge to edge; the brand and the links sit over the same column as
the content underneath, so a reader's eye follows one vertical line down the page instead of
two. Give the bar the same `container max` as `main` — matching tiers is what makes the edges
coincide.

Pass `max="none"` when the content genuinely wants the window — a table, a board, a canvas.
Pass a narrower step when it is prose.

:::preview{title="The rule keeps the window's width; the items follow the content" wide bleed height="22rem"}
<x-wirekit::app-shell viewport>
    <x-slot:header>
        <x-wirekit::navbar container max="md">
            <x-slot:brand>
                <x-wirekit::text weight="semibold">Acme</x-wirekit::text>
            </x-slot:brand>
            <x-wirekit::navbar.item href="#" active>Handbook</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Changelog</x-wirekit::navbar.item>
        </x-wirekit::navbar>
    </x-slot:header>

    <x-wirekit::main max="md">
        <x-wirekit::stack gap="md">
            <x-wirekit::heading level="1" size="lg">Working agreements</x-wirekit::heading>
            <x-wirekit::text>The bar above still runs edge to edge, and this column does not. On a wide monitor that difference is what keeps the page readable — the chrome belongs to the window, the prose belongs to the eye.</x-wirekit::text>
        </x-wirekit::stack>
    </x-wirekit::main>
</x-wirekit::app-shell>
:::

## Everything at once

The previews above each hold one decision still. This one holds none of them still: the bar
carries a brand, a navigation row and an action cluster with an account menu behind it; a
second bar under it carries the trail; the content sits above a real page footer.
It is the one to paste when you are starting an application rather than reading about one.

::: 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="Stacked Shell — every zone filled" wide bleed height="34rem"}
<x-wirekit::app-shell viewport>
    <x-slot:header>
        <x-wirekit::navbar>
            <x-slot:brand>
                <x-wirekit::avatar initials="AC" size="sm" />
                <x-wirekit::text weight="semibold">Acme</x-wirekit::text>
            </x-slot:brand>

            <x-wirekit::navbar.item href="#" active>Overview</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Projects</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Reports</x-wirekit::navbar.item>
            <x-wirekit::navbar.item href="#">Team</x-wirekit::navbar.item>

            <x-slot:actions>
                <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Notifications">
                    <x-wirekit::icon name="bell" size="sm" />
                </x-wirekit::button>

                <x-wirekit::dropdown placement="bottom-end">
                    <x-slot:trigger>
                        <x-wirekit::button intent="neutral" surface="ghost" size="sm" aria-label="Account menu">
                            <x-wirekit::avatar initials="DO" size="sm" />
                        </x-wirekit::button>
                    </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-slot:actions>
        </x-wirekit::navbar>

        <x-wirekit::shell-bar label="Page location">
            {{-- `schema="false"` because a preview is a fragment on a page that already has
                 its own structured data; a second trail would claim the page is somewhere it
                 is not. The copy-paste fence below deliberately omits it. --}}
            <x-wirekit::breadcrumb schema="false" :items="[
                ['label' => 'Projects', 'href' => '#'],
                ['label' => 'Atlas', 'href' => '#'],
                ['label' => 'Milestones'],
            ]" />

            <x-slot:end>
                <x-wirekit::button intent="neutral" surface="outline" size="sm">Share</x-wirekit::button>
                <x-wirekit::button size="sm">New milestone</x-wirekit::button>
            </x-slot:end>
        </x-wirekit::shell-bar>
    </x-slot:header>

    <x-wirekit::main>
        <x-wirekit::stack gap="lg">
            <x-wirekit::heading level="1" size="lg">Milestones</x-wirekit::heading>

            <x-wirekit::stats cols="3">
                <x-wirekit::stat label="Shipped" value="14" change="+3" trend="up" />
                <x-wirekit::stat label="In flight" value="5" />
                <x-wirekit::stat label="Overdue" value="2" change="+1" trend="down" />
            </x-wirekit::stats>

            <x-wirekit::card>
                <x-wirekit::card.body>
                    <x-wirekit::stack gap="sm">
                        <x-wirekit::heading level="2" size="sm">Atlas 2.0</x-wirekit::heading>
                        <x-wirekit::text intent="muted">The bars stay where they are. Only this region scrolls, which is what makes the shape a shell rather than a page with a header on top.</x-wirekit::text>
                    </x-wirekit::stack>
                </x-wirekit::card.body>
            </x-wirekit::card>
        </x-wirekit::stack>

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

## The row is the navigation, and it has a ceiling

- **One `nav` landmark**, named by the navbar. The trail gets a second, named by the
  `label` on its bar — two landmarks of the same role need different names, or a screen
  reader lists them identically.
- **`aria-current="page"`** comes from `:active` on the item. It is the only thing that tells
  a non-visual reader which section they are in; the visual treatment says nothing to them.
- **The navbar collapses to a menu below `md`** and brings its own toggle with it. This shell
  has no navigation column, so it opens no drawer of its own and there is exactly one menu
  on the page.
- **The ceiling is about seven items.** Past that the row starts hiding sections behind an
  overflow menu, and a hidden section is one a reader will not find. That is the signal to
  move to a column, not to make the row scroll.

## Customization

- **The bar's width and the content's width are separate decisions**, and
  [Giving the content a width](#giving-the-content-a-width) is the section that walks them. A
  full-bleed bar over a boxed column is the common shape and it is two props, not a wrapper.
- **The trail is optional and takes its own bar.** [Passing the trail](#passing-the-trail)
  covers what it needs; a breadcrumb crammed into the navigation row is the arrangement that
  looks fine at desktop and wraps into two lines on the first laptop.
- **The row's ceiling is a real limit**, not a style preference — see
  [The row is the navigation, and it has a ceiling](#the-row-is-the-navigation-and-it-has-a-ceiling).
  Past it, this is the wrong shell.

## Production Considerations

- **A horizontal navigation row does not scale with the application.** Every section added is a
  section every reader scans, and the row runs out of width before the product runs out of
  areas — planning for that at the start is cheaper than migrating shells later.
- **The active section comes from the server**, for the same reason it does in every other
  shell: a first paint with nothing marked is a highlight that arrives late on every navigation.
- **Sticky bars cost their height on every screen**, and on a phone in landscape that is a
  substantial share of it. Decide whether the bar sticks per breakpoint rather than globally.
- **The trail is generated from the route, not written by hand.** A breadcrumb maintained
  separately from the routing is one that disagrees with the address bar within a release.

## Accessibility

The reasoning lives in
[The row is the navigation, and it has a ceiling](#the-row-is-the-navigation-and-it-has-a-ceiling),
and the shape is:

- **Two `nav` landmarks with different names** — the row and the trail. Two of the same role
  sharing a name is what makes a screen reader list them identically.
- **`aria-current="page"`** from `:active` on the item, which is the only signal a non-visual
  reader gets about where they are.
- **A skip link before the row**, targeting `main`, so a keyboard reader is not required to tab
  the whole navigation on every page.
- **The overflow control, if the row has one, is a button with `aria-expanded`** — a row that
  hides items behind a menu has to announce that the menu is there.

## Related

- [Sidebar Shell](/blueprints/application-shells/sidebar-shell) — the same application once the sections stop fitting in a row
- [Navbar](/components/navbar) — the bar itself, usable without a shell
- [Breadcrumb](/components/breadcrumb) — the trail
- [App Shell](/components/app-shell) — the primitive this page composes
