App layouts

Shells for authenticated application screens.

Every shell on this page binds the same three pieces: a .navbar, a .sidebar carrying a .sidebar-nav, and a main region. The grid that binds them is the pattern. The pieces have their own component pages, and the small layouts inside them are on the composition pages.

Why a grid

Flexbox lays out one dimension at a time, which works while the regions nest cleanly. A shell usually does not: the header spans the full width while the sidebar runs beside it and below it. Nested flex containers can do it, but the layout ends up spread across the markup instead of defined in one place.

A named grid-template-areas is that one place. The value defines the shell as rows and columns of named areas, and a sidebar that crosses the header occupies two rows of it. Rearranging for small screens means editing the areas value, not re-nesting the markup. Every pattern here uses one grid.

The demos and their queries

Media queries resolve against the viewport, and dragging a demo frame does not change the viewport. So the shells below use container queries instead: a parent carries .cq-inline, and every @container rule in the demos resolves against the width you can see changing.

The show-and-hide widths are written into each demo's own CSS, and they are smaller than you would use in production so the previews start expanded. Choose your own on a real page. Where a width matches one of the library breakpoints, the .cq-hide-* utilities handle the hiding for container queries, and the plain .hide-* utilities cover the @media case. Both sets are on the utilities page. The last demo shows the queries staying container-scoped when the shell is itself a column inside something larger.

Two demos include a few lines of script. The state is carried by ARIA attributes: the collapse toggle flips aria-pressed, the master-detail rows flip aria-expanded, and :has() turns those attributes into layout. Real code would also route or persist state, and none of that changes the CSS shown here.

Dashboard

Header on top, sidebar and content side by side beneath it: two rows and two columns, with the header spanning both columns.

The responsive half is mostly not in the grid. Below 36rem of width, the sidebar is hidden and a hamburger button appears. The sidebar's auto track has nothing left to size, so it collapses to zero and main goes full width. There is no second grid definition and no breakpoint rule on the grid itself.

Dashboard

Drag the frame narrow. The sidebar hands off to the hamburger, and the grid needs no rule of its own to absorb the change.

<style>
                .app-dashboard {
                    display: grid;
                    grid-template-columns: auto minmax(0, 1fr);
                    grid-template-rows: auto 1fr;
                    grid-template-areas:
                        'header  header'
                        'sidebar main';
                    min-block-size: 100dvh;
                }
                .app-dashboard > .navbar {
                    grid-area: header;
                }
                .app-dashboard > .sidebar {
                    grid-area: sidebar;
                    --sidebar-width: 10rem;
                }
                .app-dashboard > main {
                    grid-area: main;
                    min-inline-size: 0;
                    padding: var(--space-md);
                }
                /* The menu button stays with the sidebar: hidden while the
                   sidebar is in flow, revealed when it leaves. */
                .app-dashboard .app-menu {
                    display: none;
                }
                @container (max-width: 36rem) {
                    .app-dashboard > .sidebar {
                        display: none;
                    }
                    .app-dashboard .app-menu {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-dashboard">
                    <header class="navbar">
                        <div class="cluster is-between">
                            <div class="cluster is-tight">
                                <button
                                    class="button is-ghost is-icon app-menu"
                                    type="button"
                                    aria-label="Menu"
                                    disabled
                                >
                                    <svg
                                        width="16"
                                        height="16"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <line x1="3" y1="6" x2="21" y2="6" />
                                        <line x1="3" y1="12" x2="21" y2="12" />
                                        <line x1="3" y1="18" x2="21" y2="18" />
                                    </svg>
                                </button>
                                <strong>App</strong>
                            </div>
                            <button
                                class="button is-ghost is-icon"
                                type="button"
                                aria-label="Account"
                                disabled
                            >
                                <svg
                                    width="16"
                                    height="16"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
                                    <circle cx="12" cy="7" r="4" />
                                </svg>
                            </button>
                        </div>
                    </header>
                    <aside class="sidebar">
                        <nav class="sidebar-nav" aria-label="Main">
                            <a class="sidebar-link" href="#" aria-current="page">Home</a>
                            <a class="sidebar-link" href="#">Users</a>
                            <a class="sidebar-link" href="#">Settings</a>
                        </nav>
                    </aside>
                    <main>
                        <h3>Dashboard</h3>
                        <p class="text-muted text--1">
                            Drag the frame narrow. The sidebar hands off to the hamburger, and the
                            grid needs no rule of its own to absorb the change.
                        </p>
                    </main>
                </div>
            </div>

Full-height sidebar

The sidebar moves to the first column and spans both rows. That is the only difference from the dashboard grid: in nested flex the same change means restructuring the markup, and here it is a change to the areas value.

Brio's own documentation uses this shape: the sidebar holds until the viewport is under 64rem, then hands off to a drawer opened from the navbar. Moving this demo's threshold to 64rem and wiring the hamburger to a brio-drawer gives the layout this documentation site uses.

Overview

The sidebar claims both rows. Narrow the frame and it steps out; the header and main were sharing a column anyway.

<style>
                .app-full-height {
                    display: grid;
                    grid-template-columns: auto minmax(0, 1fr);
                    grid-template-rows: auto 1fr;
                    grid-template-areas:
                        'sidebar header'
                        'sidebar main';
                    min-block-size: 100dvh;
                }
                .app-full-height > .sidebar {
                    grid-area: sidebar;
                    --sidebar-width: 10rem;
                }
                .app-full-height > .navbar {
                    grid-area: header;
                }
                .app-full-height > main {
                    grid-area: main;
                    min-inline-size: 0;
                    padding: var(--space-md);
                }
                .app-full-height .app-menu {
                    display: none;
                }
                @container (max-width: 36rem) {
                    .app-full-height > .sidebar {
                        display: none;
                    }
                    .app-full-height .app-menu {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-full-height">
                    <aside class="sidebar">
                        <nav class="sidebar-nav" aria-label="Main">
                            <a class="sidebar-link" href="#" aria-current="page">Overview</a>
                            <a class="sidebar-link" href="#">Reports</a>
                            <a class="sidebar-link" href="#">Team</a>
                            <a class="sidebar-link" href="#">Billing</a>
                        </nav>
                    </aside>
                    <header class="navbar">
                        <div class="cluster is-between">
                            <div class="cluster is-tight">
                                <button
                                    class="button is-ghost is-icon app-menu"
                                    type="button"
                                    aria-label="Menu"
                                    disabled
                                >
                                    <svg
                                        width="16"
                                        height="16"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <line x1="3" y1="6" x2="21" y2="6" />
                                        <line x1="3" y1="12" x2="21" y2="12" />
                                        <line x1="3" y1="18" x2="21" y2="18" />
                                    </svg>
                                </button>
                                <strong>Workspace</strong>
                            </div>
                            <button
                                class="button is-ghost is-icon"
                                type="button"
                                aria-label="Account"
                                disabled
                            >
                                <svg
                                    width="16"
                                    height="16"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
                                    <circle cx="12" cy="7" r="4" />
                                </svg>
                            </button>
                        </div>
                    </header>
                    <main>
                        <h3>Overview</h3>
                        <p class="text-muted text--1">
                            The sidebar claims both rows. Narrow the frame and it steps out; the
                            header and main were sharing a column anyway.
                        </p>
                    </main>
                </div>
            </div>

Collapsible sidebar

Brio ships the collapse: data-collapsed on the panel reduces it to an icon rail at --sidebar-collapsed-width, and the nav hides its labels. The script's only job is flipping the attribute; aria-pressed on the toggle carries the state for screen readers, and the grid's auto track follows the sidebar's width.

The motion comes from the component. The width transition uses --transition-normal and is disabled by reduced-motion settings like everything else. It animates because --sidebar-width is registered with @property as a length, in brio.css where the registration must sit unlayered; an unregistered custom property changes value without animating. Two details are left to the page: the panel's header label is the shell's own chrome, so this demo hides it while collapsed, and icon-only rows need an aria-label, because a hidden span does not contribute to the accessible name.

Dashboard

Press the chevron in the sidebar's header. The rail thins to 3rem; drag the frame past its threshold and it hides instead.

<style>
                .app-collapse {
                    display: grid;
                    grid-template-columns: auto minmax(0, 1fr);
                    grid-template-rows: auto 1fr;
                    grid-template-areas:
                        'header  header'
                        'sidebar main';
                    min-block-size: 100dvh;
                }
                .app-collapse > .navbar {
                    grid-area: header;
                }
                .app-collapse > .sidebar:not([data-collapsed]) {
                    grid-area: sidebar;
                    --sidebar-width: 10rem;
                }
                /* The panel's own label is app chrome; the nav's labels
                the component already hid. */
                .app-collapse > .sidebar[data-collapsed] > header strong {
                    display: none;
                }
                .app-collapse > main {
                    grid-area: main;
                    min-inline-size: 0;
                    padding: var(--space-md);
                }
                .app-collapse .app-menu {
                    display: none;
                }
                @container (max-width: 36rem) {
                    .app-collapse > .sidebar {
                        display: none;
                    }
                    .app-collapse .app-menu {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-collapse" id="app-collapse-demo">
                    <header class="navbar">
                        <div class="cluster is-between">
                            <div class="cluster is-tight">
                                <button
                                    class="button is-ghost is-icon app-menu"
                                    type="button"
                                    aria-label="Menu"
                                    disabled
                                >
                                    <svg
                                        width="16"
                                        height="16"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <line x1="3" y1="6" x2="21" y2="6" />
                                        <line x1="3" y1="12" x2="21" y2="12" />
                                        <line x1="3" y1="18" x2="21" y2="18" />
                                    </svg>
                                </button>
                                <strong>App</strong>
                            </div>
                            <button
                                class="button is-ghost is-icon"
                                type="button"
                                aria-label="Account"
                                disabled
                            >
                                <svg
                                    width="16"
                                    height="16"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
                                    <circle cx="12" cy="7" r="4" />
                                </svg>
                            </button>
                        </div>
                    </header>
                    <aside class="sidebar">
                        <header>
                            <div class="cluster is-between">
                                <strong>Menu</strong>
                                <button
                                    class="button is-ghost is-sm is-icon"
                                    type="button"
                                    aria-label="Toggle the sidebar"
                                    aria-pressed="false"
                                >
                                    <svg
                                        width="14"
                                        height="14"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <polyline points="11 17 6 12 11 7" />
                                        <polyline points="18 17 13 12 18 7" />
                                    </svg>
                                </button>
                            </div>
                        </header>
                        <nav class="sidebar-nav" aria-label="Main">
                            <a class="sidebar-link" href="#" aria-current="page" aria-label="Home">
                                <svg
                                    class="sidebar-link-icon"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <path d="m3 9 9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
                                </svg>
                                <span class="sidebar-link-text">Home</span>
                            </a>
                            <a class="sidebar-link" href="#" aria-label="Users">
                                <svg
                                    class="sidebar-link-icon"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2" />
                                    <circle cx="12" cy="7" r="4" />
                                </svg>
                                <span class="sidebar-link-text">Users</span>
                            </a>
                            <a class="sidebar-link" href="#" aria-label="Settings">
                                <svg
                                    class="sidebar-link-icon"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <circle cx="12" cy="12" r="3" />
                                    <path
                                        d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1z"
                                    />
                                </svg>
                                <span class="sidebar-link-text">Settings</span>
                            </a>
                        </nav>
                    </aside>
                    <main>
                        <h3>Dashboard</h3>
                        <p class="text-muted text--1">
                            Press the chevron in the sidebar's header. The rail thins to 3rem; drag
                            the frame past its threshold and it hides instead.
                        </p>
                    </main>
                </div>
            </div>
            <script>
                (() => {
                    const shell = document.getElementById('app-collapse-demo');
                    if (!shell) return;
                    shell.addEventListener('click', (e) => {
                        const toggle = e.target.closest('[aria-pressed]');
                        if (!toggle) return;
                        e.preventDefault();
                        const pressed = toggle.getAttribute('aria-pressed') !== 'true';
                        toggle.setAttribute('aria-pressed', String(pressed));
                        toggle.closest('.sidebar').toggleAttribute('data-collapsed', pressed);
                    });
                })();
            </script>

Three columns

Adding a filters panel on the trailing edge is one more column and one more auto track. The order the regions leave at small widths is the part to decide: the filters hide first, with a funnel button in the navbar showing where they went; the sidebar follows, replaced by the hamburger. An auto track with nothing in it sizes to zero, so the shell reflows as each region leaves.

Mail

Drag the frame: the filters go first and the funnel appears; the sidebar follows.

<style>
                .app-three-col {
                    display: grid;
                    grid-template-columns: auto minmax(0, 1fr) auto;
                    grid-template-rows: auto 1fr;
                    grid-template-areas:
                        'sidebar header header'
                        'sidebar main   aside';
                    min-block-size: 100dvh;
                }
                .app-three-col > .sidebar {
                    grid-area: sidebar;
                    --sidebar-width: 9rem;
                }
                .app-three-col > .navbar {
                    grid-area: header;
                }
                .app-three-col > main {
                    grid-area: main;
                    min-inline-size: 0;
                    padding: var(--space-md);
                }
                .app-three-col > .app-filters {
                    grid-area: aside;
                    inline-size: 9rem;
                    overflow-y: auto;
                    padding: var(--space-md);
                    background-color: var(--color-surface-sunken);
                    border-inline-start: var(--border-width-default) solid
                        var(--color-border-subtle);
                }
                /* The filters go first, the sidebar second; each departure
                brings its navbar stand-in forward. */
                .app-three-col .app-menu,
                .app-three-col .app-funnel {
                    display: none;
                }
                @container (max-width: 38rem) {
                    .app-three-col > .app-filters {
                        display: none;
                    }
                    .app-three-col .app-funnel {
                        display: inline-flex;
                    }
                }
                @container (max-width: 30rem) {
                    .app-three-col > .sidebar {
                        display: none;
                    }
                    .app-three-col .app-menu {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-three-col">
                    <aside class="sidebar">
                        <nav class="sidebar-nav" aria-label="Mail">
                            <a class="sidebar-link" href="#" aria-current="page">Inbox</a>
                            <a class="sidebar-link" href="#">Sent</a>
                            <a class="sidebar-link" href="#">Drafts</a>
                        </nav>
                    </aside>
                    <header class="navbar">
                        <div class="cluster is-between">
                            <div class="cluster is-tight">
                                <button
                                    class="button is-ghost is-icon app-menu"
                                    type="button"
                                    aria-label="Menu"
                                    disabled
                                >
                                    <svg
                                        width="16"
                                        height="16"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <line x1="3" y1="6" x2="21" y2="6" />
                                        <line x1="3" y1="12" x2="21" y2="12" />
                                        <line x1="3" y1="18" x2="21" y2="18" />
                                    </svg>
                                </button>
                                <strong>Mail</strong>
                            </div>
                            <button
                                class="button is-ghost is-icon app-funnel"
                                type="button"
                                aria-label="Filters"
                                disabled
                            >
                                <svg
                                    width="16"
                                    height="16"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <polygon points="22 3 2 3 10 12.46 10 19 14 21 14 12.46 22 3" />
                                </svg>
                            </button>
                        </div>
                    </header>
                    <main>
                        <h3>Mail</h3>
                        <p class="text-muted text--1">
                            Drag the frame: the filters go first and the funnel appears; the sidebar
                            follows.
                        </p>
                    </main>
                    <aside class="app-filters">
                        <strong class="text--1">Filters</strong>
                        <nav class="sidebar-nav" aria-label="Filters">
                            <a class="sidebar-link" href="#">Unread</a>
                            <a class="sidebar-link" href="#">Starred</a>
                            <a class="sidebar-link" href="#">Attachments</a>
                        </nav>
                    </aside>
                </div>
            </div>

Split view

List on the left, detail on the right. Below 32rem of container width the detail leaves the flow and the list takes the full width; opening a row shows the detail over the list, with a back button to close it. The rows carry aria-expanded, and the pane reads that attribute with :has(). Wiring the rows to real routing changes nothing in the layout CSS.

Re: design review

From sarah@example.com, 2:14 PM

Thanks for pulling these together. I went through the flows and have a few comments on the onboarding path, mostly about where the empty states go.

<style>
                .app-split {
                    position: relative;
                    display: grid;
                    grid-template-columns: 14rem minmax(0, 1fr);
                    grid-template-areas: 'list detail';
                    min-block-size: 100dvh;
                }
                .app-split > .list {
                    grid-area: list;
                    border-inline-end: var(--border-width-default) solid var(--color-border-subtle);
                    overflow-y: auto;
                    padding: var(--space-sm);
                }
                .app-split > .detail {
                    grid-area: detail;
                    overflow-y: auto;
                    background-color: var(--color-surface);
                }
                .app-split-detail-header {
                    display: flex;
                    align-items: center;
                    gap: var(--space-xs);
                    padding: var(--space-sm) var(--space-md);
                    border-block-end: var(--border-width-default) solid var(--color-border-subtle);
                }
                .app-split-detail-header .app-back {
                    display: none;
                }
                .app-split-detail-body {
                    padding: var(--space-md);
                }
                .app-split-items {
                    margin: 0;
                    padding: 0;
                    list-style: none;
                    display: grid;
                    gap: var(--space-3xs);
                }
                .app-split-row {
                    display: block;
                    padding: var(--space-2xs) var(--space-xs);
                    border-radius: var(--radius-sm);
                    color: var(--color-text);
                    text-decoration: none;
                }
                .app-split-row:hover {
                    background-color: var(--color-fill);
                }
                .app-split-row[aria-expanded='true'] {
                    background-color: var(--color-state-fill);
                }
                .app-split-row-meta {
                    display: block;
                    font-size: var(--font-size-xs);
                    color: var(--color-text-muted);
                }

                /* Narrow: list only, detail as an overlay that an open
                   row floats back in. */
                @container (max-width: 32rem) {
                    .app-split {
                        grid-template-columns: minmax(0, 1fr);
                        grid-template-areas: 'list';
                    }
                    .app-split > .list {
                        border-inline-end: 0;
                    }
                    .app-split > .detail {
                        display: none;
                    }
                    .app-split:has([aria-expanded='true']) > .detail {
                        display: block;
                        position: absolute;
                        inset: 0;
                        z-index: var(--zindex-raised);
                    }
                    .app-split-detail-header .app-back {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-split" id="app-split-demo">
                    <aside class="list" aria-label="Messages">
                        <ul class="app-split-items">
                            <li>
                                <a class="app-split-row" href="#" aria-expanded="false">
                                    <strong>Sarah Chen</strong>
                                    <span class="app-split-row-meta">Re: design review</span>
                                </a>
                            </li>
                            <li>
                                <a class="app-split-row" href="#" aria-expanded="false">
                                    <strong>Build system</strong>
                                    <span class="app-split-row-meta">Deploy succeeded</span>
                                </a>
                            </li>
                            <li>
                                <a class="app-split-row" href="#" aria-expanded="false">
                                    <strong>Alex Park</strong>
                                    <span class="app-split-row-meta">Quick question</span>
                                </a>
                            </li>
                            <li>
                                <a class="app-split-row" href="#" aria-expanded="false">
                                    <strong>Team weekly</strong>
                                    <span class="app-split-row-meta">Agenda for Thursday</span>
                                </a>
                            </li>
                        </ul>
                    </aside>
                    <article class="detail">
                        <header class="app-split-detail-header">
                            <button
                                class="button is-ghost is-icon app-back"
                                type="button"
                                aria-label="Back"
                            >
                                <svg
                                    width="16"
                                    height="16"
                                    viewBox="0 0 24 24"
                                    fill="none"
                                    stroke="currentColor"
                                    stroke-width="2"
                                    stroke-linecap="round"
                                    stroke-linejoin="round"
                                    aria-hidden="true"
                                >
                                    <polyline points="15 18 9 12 15 6" />
                                </svg>
                            </button>
                            <strong>Re: design review</strong>
                        </header>
                        <div class="flow app-split-detail-body">
                            <p class="text-muted text--1">
                                <strong>From</strong> sarah@example.com, 2:14 PM
                            </p>
                            <p>
                                Thanks for pulling these together. I went through the flows and have
                                a few comments on the onboarding path, mostly about where the empty
                                states go.
                            </p>
                        </div>
                    </article>
                </div>
            </div>
            <script>
                (() => {
                    const pane = document.getElementById('app-split-demo');
                    if (!pane) return;
                    pane.addEventListener('click', (e) => {
                        const row = e.target.closest('.app-split-row');
                        const back = e.target.closest('.app-back');
                        if (!row && !back) return;
                        e.preventDefault();
                        pane.querySelectorAll('.app-split-row').forEach((link) =>
                            link.setAttribute('aria-expanded', String(link === row))
                        );
                    });
                })();
            </script>

A split inside a dashboard

The split is a region like any other and composes into a shell the same way. The width its breakpoint resolves against is the part to note: main carries .cq-inline itself, so the split's threshold is measured in the content column, not the shell. Drag the frame and the regions leave in order, the shell's sidebar first and the split's detail later. Each container context resolves on its own.

Re: design review

From sarah@example.com

Drag the frame slowly. The shell's sidebar lets go first, and the split's detail holds on a little longer, because it listens to the content column instead of the shell.

<style>
                .app-inbox {
                    display: grid;
                    grid-template-columns: auto minmax(0, 1fr);
                    grid-template-rows: auto 1fr;
                    grid-template-areas:
                        'header  header'
                        'sidebar main';
                    min-block-size: 100dvh;
                }
                .app-inbox > .navbar {
                    grid-area: header;
                }
                .app-inbox > .sidebar {
                    grid-area: sidebar;
                    --sidebar-width: 8rem;
                }
                .app-inbox > main {
                    grid-area: main;
                    min-inline-size: 0;
                }

                /* The pane is its own query context (main carries
                   .cq-inline in the markup), so its threshold measures
                   the content column, not the shell. */
                .app-inbox-pane {
                    position: relative;
                    display: grid;
                    grid-template-columns: 12rem minmax(0, 1fr);
                    grid-template-areas: 'list detail';
                    block-size: 100%;
                }
                .app-inbox-pane > .list {
                    grid-area: list;
                    border-inline-end: var(--border-width-default) solid var(--color-border-subtle);
                    overflow-y: auto;
                    padding: var(--space-sm);
                }
                .app-inbox-pane > .detail {
                    grid-area: detail;
                    overflow-y: auto;
                    background-color: var(--color-surface);
                }
                .app-inbox-detail-header {
                    display: flex;
                    align-items: center;
                    gap: var(--space-xs);
                    padding: var(--space-sm) var(--space-md);
                    border-block-end: var(--border-width-default) solid var(--color-border-subtle);
                }
                .app-inbox-detail-header .app-back {
                    display: none;
                }
                .app-inbox-detail-body {
                    padding: var(--space-md);
                }
                .app-inbox-pane .sidebar-link[aria-expanded='true'] {
                    background-color: var(--color-state-fill);
                }

                @container (max-width: 36rem) {
                    .app-inbox > .sidebar {
                        display: none;
                    }
                    .app-inbox .app-menu {
                        display: inline-flex;
                    }
                }

                @container (max-width: 28rem) {
                    .app-inbox-pane {
                        grid-template-columns: minmax(0, 1fr);
                        grid-template-areas: 'list';
                    }
                    .app-inbox-pane > .list {
                        border-inline-end: 0;
                    }
                    .app-inbox-pane > .detail {
                        display: none;
                    }
                    .app-inbox-pane:has([aria-expanded='true']) > .detail {
                        display: block;
                        position: absolute;
                        inset: 0;
                        z-index: var(--zindex-raised);
                    }
                    .app-inbox-detail-header .app-back {
                        display: inline-flex;
                    }
                }
            </style>
            <div class="cq-inline">
                <div class="app-inbox">
                    <header class="navbar">
                        <div class="cluster is-between">
                            <div class="cluster is-tight">
                                <button
                                    class="button is-ghost is-icon app-menu"
                                    type="button"
                                    aria-label="Menu"
                                    disabled
                                >
                                    <svg
                                        width="16"
                                        height="16"
                                        viewBox="0 0 24 24"
                                        fill="none"
                                        stroke="currentColor"
                                        stroke-width="2"
                                        stroke-linecap="round"
                                        stroke-linejoin="round"
                                        aria-hidden="true"
                                    >
                                        <line x1="3" y1="6" x2="21" y2="6" />
                                        <line x1="3" y1="12" x2="21" y2="12" />
                                        <line x1="3" y1="18" x2="21" y2="18" />
                                    </svg>
                                </button>
                                <strong>Inbox</strong>
                            </div>
                        </div>
                    </header>
                    <aside class="sidebar">
                        <nav class="sidebar-nav" aria-label="Mail">
                            <a class="sidebar-link" href="#" aria-current="page">Inbox</a>
                            <a class="sidebar-link" href="#">Sent</a>
                            <a class="sidebar-link" href="#">Drafts</a>
                            <a class="sidebar-link" href="#">Archive</a>
                        </nav>
                    </aside>
                    <main class="cq-inline">
                        <div class="app-inbox-pane" id="app-inbox-demo">
                            <aside class="list" aria-label="Messages">
                                <nav class="sidebar-nav">
                                    <a class="sidebar-link" href="#" aria-expanded="false"
                                        >Sarah Chen</a
                                    >
                                    <a class="sidebar-link" href="#" aria-expanded="false"
                                        >Build system</a
                                    >
                                    <a class="sidebar-link" href="#" aria-expanded="false"
                                        >Alex Park</a
                                    >
                                </nav>
                            </aside>
                            <article class="detail">
                                <header class="app-inbox-detail-header">
                                    <button
                                        class="button is-ghost is-icon app-back"
                                        type="button"
                                        aria-label="Back"
                                    >
                                        <svg
                                            width="16"
                                            height="16"
                                            viewBox="0 0 24 24"
                                            fill="none"
                                            stroke="currentColor"
                                            stroke-width="2"
                                            stroke-linecap="round"
                                            stroke-linejoin="round"
                                            aria-hidden="true"
                                        >
                                            <polyline points="15 18 9 12 15 6" />
                                        </svg>
                                    </button>
                                    <strong>Re: design review</strong>
                                </header>
                                <div class="flow app-inbox-detail-body">
                                    <p class="text-muted text--1">
                                        <strong>From</strong> sarah@example.com
                                    </p>
                                    <p>
                                        Drag the frame slowly. The shell's sidebar lets go first,
                                        and the split's detail holds on a little longer, because it
                                        listens to the content column instead of the shell.
                                    </p>
                                </div>
                            </article>
                        </div>
                    </main>
                </div>
            </div>
            <script>
                (() => {
                    const shell = document.getElementById('app-inbox-demo');
                    if (!shell) return;
                    shell.addEventListener('click', (e) => {
                        const row = e.target.closest('.sidebar-link');
                        const back = e.target.closest('.app-back');
                        if (!row && !back) return;
                        e.preventDefault();
                        shell
                            .querySelectorAll('.sidebar-link')
                            .forEach((link) =>
                                link.setAttribute('aria-expanded', String(link === row))
                            );
                    });
                })();
            </script>