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.
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.
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.
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.
Only the list scrolls
Give the content area overflow-y: auto and the shell a fixed height. The search and the mailboxes stay in view while the messages scroll.
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 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.
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.
Guidelines
Scroll the list: the header, with search and the account, is always one click away.
Scroll this one. When the whole frame scrolls, people have to go back to the top to reach the menu.
The app's name, search, notifications and the account. Page actions belong in the page.
Buttons that only work on one page are noise on all the others, and they wrap on small screens.
Props
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
Shape & size
Type & motion
This component only
/* 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
- Keep one of each landmark per page: one header as the banner, one AppShellContent as main, one footer. Nested in other landmarks they become plain boxes, so don't put the shell inside a main.
- Name the navigation regions ("Main", "Settings sections") when there is more than one, so they can be told apart in a screen reader's list of landmarks.
- Translate the skip link's text. Give the target tabIndex={-1} if it isn't a main, so focus can land on it.
- When only the content scrolls, it must hold a focusable element or take tabIndex={0}, so keyboard users can scroll it.