// preview in
saved for every page

AppShell

The frame of an app screen. A header, the page's main area and an optional footer, in a column the height of the screen, with a skip link to jump past the header.

Pattern HTML landmarks: header, main, footerServer componentSource ↗

Installation

Set up Camaleo once (Installation), then import what you need. It has no state, so it also renders on the server.

import { AppShell, AppShellHeader, AppShellBody, AppShellContent, AppShellFooter, SkipLink } from "@camaleo/react";

Anatomy

A column the height of the screen, with the safe areas of notched phones as padding. The parts are plain landmarks: the header is the page's banner, the content is its main. For a sidebar, put AppShellBody under the header: a row with the Sidebar and AppShellContent, where the content takes the rest of the width, also beside a rail.

Examples

Every example is a real file you can copy, shrunk to fit a tile. The docs page around them already has a main, so each tile's AppShellContent takes render={<div />} and an id from useId; in your app, drop both.

minimal · light · #3fa34d
Orbita

Projects

All changes saved
Orbita

Good morning, Ana

Open tasks24
Due this week5
Done in October38

Dashboard with a rail

AppShellBody puts the sidebar in a row under the header. Press the toggle at the bottom of the rail to show the labels; the content takes the rest of the width.

Orders

OrderCustomerStatusTotal
#1042Marina CostaPaid$248.90
#1041João PereiraPending$89.00
#1040Luiza AlvesPaid$1,120.50
#1039Rafael SouzaRefunded$64.30

Breadcrumb and search in the header

The header says where you are and gives a way to jump anywhere. Press Search: the command palette opens and goes to a page.

Settings

Profile

We send receipts and alerts here.

Section links beside the form

A SidebarNav inside the content lists the sections of one page. When the frame is narrow it moves above the form instead of squeezing it.

Skip to content
Orbita

Tasks

Press Tab from the top of this frame: the first stop is a link that jumps here, past the header.

Skip link first

Click above the frame and press Tab. SkipLink appears, and Enter moves focus past the header to the content, so keyboard users don't tab through every link on every page.

Orbita

Dashboard

Wide frame: the full sidebar.

Sidebar, rail or drawer by width

The shell doesn't change by itself: you pick a layout per width. Narrow this window to see the full sidebar become a rail, then a menu button with a drawer.

In context

Phone-sized screens you can copy, in the skin you picked. Header and footer stay put; the middle scrolls.

Orbita supportReplies in about 5 minutes
  1. Hi Ana! How can we help today?
  2. I can't find where to export my October report.
  3. Go to Reports, open October and press Export. It saves a PDF.
Support chatAppShell · Input · IconButton · Avatar
CheckoutStep 2 of 3
Order summary
Coffee beans, 1 kg$32.00
Paper filters (100)$6.50
Ceramic mug$18.00
Shipping$5.90
Deliver toAna Ribeiro, 210 Pine Street, Apt 4B, Portland, OR 97204
Total$62.40
CheckoutAppShell · Button · Separator
Hi, Ana
Available balance$3,284.15
Recent activity
  • Corner Market10/29/2026−$54.20
  • Salary10/28/2026+$4,850.00
  • Electric bill10/27/2026−$96.40
  • Transfer to Marina10/26/2026−$120.00
Banking app with a tab barAppShell · Card · Avatar

Guidelines

DoOnly the content scrolls.

Scroll the list: the header, with search and the account, is always one click away.

Don’tThe header scrolls away.

Scroll this one. When the whole frame scrolls, people have to go back to the top to reach the menu.

Orbita
DoWhat every page needs.

The app's name, search, notifications and the account. Page actions belong in the page.

Orbita
Don’tEvery action in the header.

Buttons that only work on one page are noise on all the others, and they wrap on small screens.

Props

PropTypeDefaultDescription
targetIdstring"main-content"The id of the element it jumps to, usually AppShellContent.

AppShell, AppShellHeader and AppShellFooter take the props of a div, header and footer. AppShellBody takes the props of a div. AppShellContent takes the props of main, and its id defaults to "main-content", the target SkipLink uses when you don't pass targetId. Change both together, and keep the id unique. render swaps the main for another element, such as render={<div />} when the page already has a main; the id stays, so the skip link still works.

Styling hooks

Restyle it without forking: target the slots, or set the tokens on a parent, a skin or the shell itself.

Tokens it reads

Color

--cml-border--cml-fg--cml-focus-ring--cml-surface

Shape & size

--cml-border-width--cml-radius-control

Type & motion

--cml-duration-fast--cml-font-body

This component only

--cml-app-shell-bg--cml-app-shell-footer-bg--cml-app-shell-header-bg--cml-app-shell-header-height
/* a taller header, and a content area that scrolls on its own */
.app [data-slot="app-shell"] {
  --cml-app-shell-header-height: 4rem;
  height: 100dvh;
}
.app [data-slot="app-shell-content"] {
  overflow-y: auto;
}

Accessibility

TabFrom the top of the page, the first stop is the skip link, which appears where it is.
EnterOn the skip link, moves focus to the content, past the header and the sidebar.
Shift + TabGoes back through the header's controls.
Was this page helpful? · Edit this page on GitHub