---
name: "App shell"
description: "A fixed sidebar and top bar around one scrolling content area, with bordered panels on a 12-column grid."
type: pattern
tags: ["Sidebar", "Dashboard", "Product UI", "Admin"]
container: "Full viewport shell: grid of sidebar and content area, 100svh tall"
max_width: "Fluid. Keep text blocks at 720px or less"
columns: "Shell: 232px sidebar and fluid area. Panels: 12 columns, spans of 5, 7 or 12"
gutter: "16px between panels"
rules: "1px solid, ink at 9% opacity, between sidebar, bar, content and status strip"
section_padding: "32px content padding, 24px inside panels, 20px below 800px"
breakpoints: "Below 800px the sidebar becomes a horizontal strip above the content and panels stack"
sticky: "Sidebar, top bar and status strip are pinned by the shell. Only the content area scrolls"
---

# App shell

A fixed sidebar and top bar around one scrolling content area, with bordered panels on a 12-column grid.

## AI build instructions

> Read this section before writing any code. It describes page structure only.
> Colors, type, radii and shadows come from the project's DESIGN.md, never from this file.

### 1. Your role

Build the page layout as **App shell**. Use the HTML and CSS under Reference implementation as the
structural source of truth. Keep the project's own copy, components and styling, and place them into the
regions listed below. If the project has a DESIGN.md, map its tokens onto the base variables
(`--paper`, `--paper-2`, `--ink`, `--ink-2`, `--ink-3`, `--line`, `--accent`) and leave the structure CSS unchanged.

### 2. Structure contract

| Token | Value |
|-------|-------|
| container | Full viewport shell: grid of sidebar and content area, 100svh tall |
| max width | Fluid. Keep text blocks at 720px or less |
| columns | Shell: 232px sidebar and fluid area. Panels: 12 columns, spans of 5, 7 or 12 |
| gutter | 16px between panels |
| rules | 1px solid, ink at 9% opacity, between sidebar, bar, content and status strip |
| section padding | 32px content padding, 24px inside panels, 20px below 800px |
| breakpoints | Below 800px the sidebar becomes a horizontal strip above the content and panels stack |
| sticky | Sidebar, top bar and status strip are pinned by the shell. Only the content area scrolls |
| accessibility | Sidebar is a nav landmark with aria-current on the active page, the content area is the main landmark, and focus order is sidebar, bar, content |

### 3. Regions

The markup marks each region with `data-region`. These attributes only drive the Blueprint view on
freedesignmd.com and can be removed in production.

1. **Sidebar.** A 232px column with the brand, the primary navigation and the page index. It has its own scroll and is never pushed by the content.
2. **Top bar.** A 48px bar over the content area: breadcrumb on the left, actions on the right. It stays fixed.
3. **Content area.** The only region that scrolls. 32px of padding and a fluid width, capped so panels stay readable.
4. **Panel.** Content comes in bordered panels with an 8px radius on a 12-column grid with a 16px gap. Panels span 5, 7 or 12 columns.
5. **Status strip.** A 40px strip pinned to the bottom of the content area for footer links.

### 4. Rules

- Make the shell exactly viewport height with a 232px sidebar. Only the content area scrolls.
- Pin the top bar at 48px and the status strip at 40px.
- Arrange content in bordered panels on a 12-column grid with a 16px gap. Panels span 5, 7 or 12.
- Mark the active sidebar item with aria-current and a surface step, never a color fill.
- Below 800px turn the sidebar into a horizontal strip and let the page scroll normally.

### 5. Hard constraints

Never do any of the following without an explicit instruction from the user:

- Change the structure tokens above, add a column, or add dividers the pattern does not define.
- Replace the structure with cards, shadows or colored backgrounds to separate sections.
- Use gradients, glows or emoji to decorate the layout.
- Put page content inside a region that is meant for another kind of content.

### 6. Before you finish, verify

- [ ] Every region listed above exists and is in the same order as in the reference markup.
- [ ] Container width, columns, gutter, rule weight and section padding match the contract.
- [ ] The layout collapses as described at the stated breakpoints, with no horizontal scroll at 390px.
- [ ] Sticky elements behave as stated and never cover focused content.
- [ ] Colors, type and radii were taken from DESIGN.md, not invented.

---

## Overview

The structure of a product, not a website. A fixed sidebar carries navigation, a fixed top bar carries context and actions, and a single content area scrolls between them. Content is arranged in bordered panels on a 12-column grid. Because everything except the content is pinned, users never lose their place, and the same shell can host any number of screens.

## When to use it

- Dashboards, admin tools and settings screens.
- Product UIs with many screens that share navigation.
- Documentation or marketing pages that want to feel like the product itself.
- Anything where context and navigation must stay visible while content scrolls.

## When to avoid it

- Marketing pages for first-time visitors, where a sidebar adds friction.
- Small-screen-first products, since the shell is built for desktop widths.
- Single-screen tools with no navigation.

---

## Reference implementation

Plain HTML and CSS. The preview on freedesignmd.com renders exactly these three blocks, so what you see
is what you get. The pattern CSS decides structure. The base CSS is the neutral skin shared by every
pattern: replace its variables with your DESIGN.md tokens and keep the names.

### HTML

```html
<!-- data-region and data-dim only drive the Blueprint view. Remove them in production. -->
<div class="app">
  <aside class="side" data-region="Sidebar" data-n="1" data-at="in" data-dim="232" data-dim-axis="tag" data-dim-at="in">
    <a class="brand" href="#"><span class="mark"></span>Meridian</a>
    <nav aria-label="Primary">
      <ul class="menu">
        <li><a href="#" class="on" aria-current="page">Product</a></li>
        <li><a href="#">Pricing</a></li>
        <li><a href="#">Docs</a></li>
        <li><a href="#">Changelog</a></li>
      </ul>
    </nav>
    <p class="eyebrow">On this page</p>
    <ul class="menu">
      <li><a href="#features">Features</a></li>
      <li><a href="#numbers">Numbers</a></li>
      <li><a href="#customers">Customers</a></li>
      <li><a href="#pricing">Pricing</a></li>
    </ul>
  </aside>
  <div class="area">
    <header class="top" data-region="Top bar" data-n="2" data-at="in" data-dim="48" data-dim-axis="tag" data-dim-at="in">
      <span class="eyebrow">Meridian / Release 2.4</span>
      <div class="actions">
        <a class="btn btn-ghost" href="#">Sign in</a>
        <a class="btn btn-primary" href="#">Start free</a>
      </div>
    </header>
    <main class="content" data-region="Content area" data-n="3" data-at="in" data-dim="32" data-dim-axis="pad">
      <div class="page-head">
        <p class="eyebrow">Release 2.4</p>
        <h1>Ship changes your customers actually read</h1>
        <p class="lead">Meridian turns merged pull requests into clear release notes, changelogs and update emails.</p>
        <div class="actions">
          <a class="btn btn-primary" href="#">Start free</a>
          <a class="btn btn-ghost" href="#">Read the docs</a>
        </div>
      </div>
      <div class="panels" data-dim="gap 16" data-dim-axis="tag" data-dim-at="in">
        <section class="panel p-12" data-region="Panel" data-n="4">
          <header class="panel-head">
            <div>
              <p class="eyebrow">01 Features</p>
              <h2>Everything between merge and inbox</h2>
            </div>
            <p class="sub">Drafts group by product area, not by commit.</p>
          </header>
          <div class="cols">
            <article class="feature">
              <span class="n">01</span>
              <h3>Draft from pull requests</h3>
              <p>Group merged work by product area and get a first draft in seconds.</p>
            </article>
            <article class="feature">
              <span class="n">02</span>
              <h3>Edit in one place</h3>
              <p>Rewrite, reorder and approve notes with your team before anything ships.</p>
            </article>
            <article class="feature">
              <span class="n">03</span>
              <h3>Publish everywhere</h3>
              <p>Send the same note to your changelog, email list and in-app feed.</p>
            </article>
          </div>
        </section>
        <section class="panel p-12" data-region="Panel">
          <header class="panel-head">
            <div>
              <p class="eyebrow">02 Numbers</p>
              <h2>Quiet, fast and always on</h2>
            </div>
            <p class="sub">Rolling twelve month medians across all workspaces.</p>
          </header>
          <div class="cols">
            <div class="stat"><strong>2.4M</strong><span>Notes published</span></div>
            <div class="stat"><strong>99.98%</strong><span>Uptime, last 12 months</span></div>
            <div class="stat"><strong>140 ms</strong><span>Median render time</span></div>
          </div>
        </section>
        <section class="panel p-7" data-region="Panel">
          <header class="panel-head">
            <div>
              <p class="eyebrow">03 Customers</p>
              <h2>In their words</h2>
            </div>
            <p class="sub">Halden, a 40 person team, publishes every week.</p>
          </header>
          <figure class="quote">
            <blockquote>“Our changelog went from a Friday chore to the page customers open first.”</blockquote>
            <figcaption><b>Sana Okafor</b> Head of Product, Halden</figcaption>
          </figure>
        </section>
        <section class="panel p-5" data-region="Panel">
          <header class="panel-head">
            <div>
              <p class="eyebrow">04 Pricing</p>
              <h2>Free for small teams</h2>
            </div>
            <p class="sub">Up to five people and one changelog, no card needed.</p>
          </header>
          <div class="actions">
            <a class="btn btn-primary" href="#">Start free</a>
          </div>
        </section>
      </div>
    </main>
    <footer class="status" data-region="Status strip" data-n="5" data-at="in">
      <div class="foot">
        <span>© 2026 Meridian</span>
        <ul>
          <li><a href="#">Privacy</a></li>
          <li><a href="#">Terms</a></li>
          <li><a href="#">Status</a></li>
        </ul>
      </div>
    </footer>
  </div>
</div>
```

### CSS (pattern)

```css
:root {
  --side: 232px;   /* sidebar */
  --bar: 48px;     /* top bar */
  --status: 40px;  /* status strip */
  --pad: 32px;     /* content padding */
  --gap: 16px;     /* panel gap */
}

/* The shell fills the viewport. Only .content scrolls. */
.app { display: grid; grid-template-columns: var(--side) minmax(0, 1fr); height: 100svh; }
.side { display: flex; flex-direction: column; gap: 16px; padding: 16px 12px; overflow: auto; border-right: 1px solid var(--line); background: var(--paper-2); }
.side .brand { padding: 0 8px; height: 32px; }
.side .eyebrow { margin-top: 8px; padding: 0 8px; }
.menu { display: grid; gap: 2px; }
.menu a { display: block; padding: 6px 8px; border-radius: 6px; font-size: 14px; color: var(--ink-2); }
.menu a:hover { color: var(--ink); }
.menu a.on { background: var(--paper); box-shadow: 0 0 0 1px var(--line); color: var(--ink); }

.area { display: flex; flex-direction: column; min-width: 0; min-height: 0; }
.top { flex: none; display: flex; align-items: center; justify-content: space-between; height: var(--bar); padding-inline: var(--pad); border-bottom: 1px solid var(--line); }
.top .btn { height: 32px; }
.content { flex: 1; min-height: 0; overflow: auto; padding: var(--pad); }
.status { flex: none; height: var(--status); padding-inline: var(--pad); border-top: 1px solid var(--line); }
.status .foot { height: 100%; }

.page-head { max-width: 720px; }
.page-head > * + * { margin-top: 12px; }
.page-head > h1 { margin-top: 12px; font-size: 28px; line-height: 1.15; letter-spacing: -0.02em; }
.page-head > .lead { font-size: 15px; }
.page-head > .actions { margin-top: 20px; }

.panels { display: grid; grid-template-columns: repeat(12, minmax(0, 1fr)); gap: var(--gap); margin-top: 32px; }
.panel { min-width: 0; padding: 24px; border: 1px solid var(--line); border-radius: 8px; background: var(--paper); }
.p-12 { grid-column: span 12; }
.p-7 { grid-column: span 7; }
.p-5 { grid-column: span 5; }
.panel-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 24px; margin-bottom: 20px; }
.panel-head > div > * + * { margin-top: 6px; }
.panel-head h2 { font-size: 17px; line-height: 1.3; letter-spacing: -0.01em; }
.panel-head .sub { text-align: right; font-size: 13px; }
.p-7 .panel-head, .p-5 .panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
.p-7 .panel-head .sub, .p-5 .panel-head .sub { text-align: left; }
.panel .quote blockquote { font-size: 17px; line-height: 1.45; }
.panel .stat strong { font-size: 28px; }
.cols { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 24px; }

@media (max-width: 800px) {
  :root { --pad: 20px; }
  .app { display: block; height: auto; }
  .side { flex-direction: row; align-items: center; gap: 4px; padding: 8px 12px; border-right: 0; border-bottom: 1px solid var(--line); overflow-x: auto; }
  .side .eyebrow, .side .menu + .menu { display: none; }
  .side .menu { display: flex; }
  .content { overflow: visible; }
  .p-12, .p-7, .p-5 { grid-column: span 12; }
  .cols { grid-template-columns: 1fr; }
  .panel-head { flex-direction: column; align-items: flex-start; gap: 6px; }
  .panel-head .sub { text-align: left; }
}
```

### CSS (base, shared by every pattern)

```css
/* Base: tokens and content blocks shared by every pattern. Structure lives in the pattern CSS. */
:root {
  color-scheme: light;
  --paper: #ffffff;
  --paper-2: #f5f5f6;
  --ink: #0e0e10;
  --ink-2: #55555c;
  --ink-3: #767680;
  --line: rgba(14, 14, 16, 0.09);
  --line-strong: rgba(14, 14, 16, 0.16);
  --accent: #c8421a;
  --inv-paper: #0b0b0d;
  --inv-ink: #f3f3f5;
  --inv-ink-2: #a4a4ab;
  --inv-line: rgba(255, 255, 255, 0.12);
  --font: "Inter Variable", Inter, ui-sans-serif, system-ui, -apple-system, "Segoe UI", sans-serif;
  --mono: "JetBrains Mono Variable", "JetBrains Mono", ui-monospace, "SF Mono", Menlo, monospace;
}
:root[data-theme="dark"] {
  color-scheme: dark;
  --paper: #0b0b0d;
  --paper-2: #131316;
  --ink: #f3f3f5;
  --ink-2: #a4a4ab;
  --ink-3: #74747c;
  --line: rgba(255, 255, 255, 0.09);
  --line-strong: rgba(255, 255, 255, 0.16);
  --accent: #ff8a5c;
  --inv-paper: #f6f6f7;
  --inv-ink: #0e0e10;
  --inv-ink-2: #55555c;
  --inv-line: rgba(14, 14, 16, 0.1);
}

*, *::before, *::after { box-sizing: border-box; }
html { -webkit-text-size-adjust: 100%; }
body {
  margin: 0;
  background: var(--paper);
  color: var(--ink);
  font: 400 15px/1.55 var(--font);
  -webkit-font-smoothing: antialiased;
}
h1, h2, h3, p, ul, ol, figure, blockquote, dl, dd { margin: 0; }
ul, ol { padding: 0; list-style: none; }
a { color: inherit; text-decoration: none; }

/* Type scale: 12 / 13 / 15 / 17 / 22 / 28 / 44 */
h1, .t-h1 { font-size: 44px; line-height: 1.05; letter-spacing: -0.03em; font-weight: 500; text-wrap: balance; }
h2, .t-h2 { font-size: 28px; line-height: 1.15; letter-spacing: -0.02em; font-weight: 500; text-wrap: balance; }
h3 { font-size: 15px; line-height: 1.4; font-weight: 500; }
.lead { font-size: 17px; line-height: 1.55; color: var(--ink-2); max-width: 46ch; text-wrap: pretty; }
.sub { font-size: 14px; line-height: 1.55; color: var(--ink-2); max-width: 40ch; text-wrap: pretty; }
.eyebrow { font: 400 12px/16px var(--mono); color: var(--ink-3); letter-spacing: 0.02em; }

/* Buttons: one primary per view */
.actions { display: flex; flex-wrap: wrap; align-items: center; gap: 8px; }
.btn {
  display: inline-flex; align-items: center; height: 36px; padding: 0 14px;
  border: 1px solid transparent; border-radius: 8px;
  font: 500 14px/1 var(--font); white-space: nowrap;
}
.btn-primary { background: var(--ink); color: var(--paper); }
.btn-ghost { border-color: var(--line-strong); color: var(--ink); }

/* Nav */
.nav { display: flex; align-items: center; gap: 32px; height: 56px; }
.brand { display: inline-flex; align-items: center; gap: 8px; font-weight: 500; letter-spacing: -0.01em; }
.mark { width: 16px; height: 16px; border: 4px solid var(--ink); border-radius: 50%; }
.links { display: flex; gap: 24px; font-size: 14px; color: var(--ink-2); }
.nav .actions { margin-left: auto; }

/* Feature */
.feature { display: flex; flex-direction: column; gap: 8px; }
.feature .n { font: 400 12px/16px var(--mono); color: var(--accent); }
.feature p { font-size: 14px; line-height: 1.55; color: var(--ink-2); }

/* Stat */
.stat strong {
  display: block; font: 500 36px/1.1 var(--font); letter-spacing: -0.03em;
  font-variant-numeric: tabular-nums;
}
.stat span { display: block; margin-top: 4px; font-size: 13px; color: var(--ink-2); }

/* Quote */
.quote blockquote { font-size: 22px; line-height: 1.35; letter-spacing: -0.015em; text-wrap: balance; }
.quote figcaption { margin-top: 16px; font-size: 13px; color: var(--ink-2); }
.quote figcaption b { color: var(--ink); font-weight: 500; }

/* Release card: the one visual used when a pattern has a media slot */
.mock { border: 1px solid var(--line-strong); border-radius: 12px; background: var(--paper); overflow: hidden; }
.mock-bar { display: flex; justify-content: space-between; padding: 12px 16px; border-bottom: 1px solid var(--line); font: 400 12px/16px var(--mono); color: var(--ink-3); }
.mock li { display: flex; gap: 12px; align-items: baseline; padding: 12px 16px; border-bottom: 1px solid var(--line); font-size: 14px; }
.mock li:last-child { border-bottom: 0; }
.mock li b { flex: none; width: 64px; font: 400 12px/16px var(--mono); color: var(--ink-3); }
.mock li b.new { color: var(--accent); }

/* Footer strip */
.foot { display: flex; align-items: center; justify-content: space-between; gap: 16px; height: 56px; font-size: 13px; color: var(--ink-3); }
.foot ul { display: flex; gap: 20px; }

/* Inverted band: swaps the tokens, so every block inside adapts */
.inv {
  --paper: var(--inv-paper); --ink: var(--inv-ink); --ink-2: var(--inv-ink-2);
  --ink-3: var(--inv-ink-2); --line: var(--inv-line); --line-strong: var(--inv-line);
  background: var(--paper); color: var(--ink);
}

@media (max-width: 640px) {
  h1, .t-h1 { font-size: 34px; }
  h2, .t-h2 { font-size: 24px; }
  .links, .nav .btn-ghost { display: none; }
  .nav { gap: 16px; }
  .foot ul { gap: 14px; }
}

```
