App Shell
The frame of an app screen: header, sidebar, main content and footer. It fills the screen, Main scrolls between the bars, and the sidebar becomes a drawer on phones.Dashboard
Preview
Code
Active sites128
Partners42
Reports due7
Households reached18.4k
Installation
Package
CLI (copy source)
pnpm
pnpm add @advui/corenpm
npm install @advui/coreUsage
import { AppShell, Sidebar } from '@advui/core'
<AppShell sidebarBreakpoint="md">
<AppShell.Header>
<AppShell.SidebarTrigger />
…
</AppShell.Header>
<AppShell.Sidebar aria-label="Main">
<Sidebar>…</Sidebar>
</AppShell.Sidebar>
<AppShell.Main>…</AppShell.Main>
<AppShell.Footer>…</AppShell.Footer>
</AppShell>Examples
Docs
`layout="sidebar-full"` with `sidebarBreakpoint="lg"`: the sidebar spans the height, and is a drawer below lg.Preview
Code
API reference
AppShell
The screen: `100dvh` tall on web and `flex: 1` on iOS and Android (a `height` prop overrides it). Its children are the four areas, each at most once, as direct children; they can come in any order.| Prop | Type | Default | Description |
|---|---|---|---|
layout | 'header-full' | 'sidebar-full' | 'header-full' | Which spans its whole edge: with `header-full` the header and footer span the width and the sidebar sits between them; with `sidebar-full` the sidebar spans the height and the header and footer sit beside it. |
sidebarBreakpoint | 'sm' | 'md' | 'lg' | 'xl' | 'md' | Below this width the sidebar is a drawer from the left, opened by `AppShell.SidebarTrigger`. |
drawerWidth | number | 280 | Width of the drawer, in px, at most 85% of the screen. |
sidebarOpen | boolean | — | Whether the drawer is open (controlled). Always closed at desktop sizes. |
defaultSidebarOpen | boolean | false | Starting `sidebarOpen` when uncontrolled. |
onSidebarOpenChange | (open: boolean) => void | — | Called with the new `sidebarOpen`, including `false` when the window widens past the breakpoint. |
stickyHeader | boolean | true | Keep the header in place while Main scrolls. With `false` it scrolls away at the top of Main, so it spans Main’s column only. |
safeArea | boolean | true | Pad the edges the shell touches for the notch and home indicator. Turn it off when the shell sits below a navigation header or above a tab bar, which pad them already. The drawer always pads.iOS, Android only. |
avoidKeyboard | boolean | true | Main keeps the focused field above the keyboard, and taps on buttons work while the keyboard is up. Turn it off when the app already avoids the keyboard around the shell.iOS, Android only. |
AppShell.Header
The top bar: a row with a border below, `<header>` (banner landmark) on web. Pads the top safe area on iOS and Android.AppShell.Sidebar
The side area, a `<nav>` from the breakpoint up. It is as wide as its content, so a collapsible Sidebar inside still shrinks. Below the breakpoint its children move into the drawer. A Sidebar inside drops its own landmark, and in the drawer it fills the width and closes the drawer when an item is pressed.| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | 'Main' | Names the navigation landmark and the drawer. |
AppShell.Main
The content, `<main>` on web. It sits in the area that scrolls (a ScrollView on iOS and Android) and fills it, so short content keeps the footer at the bottom. Add padding or a Container inside.AppShell.Footer
The bottom bar, `<footer>` (contentinfo landmark) on web. It stays at the bottom edge; for a footer at the end of the content, put it at the end of Main instead. Pads the bottom safe area on iOS and Android.AppShell.SidebarTrigger
An IconButton that opens the drawer, shown only below the breakpoint. It reports `aria-expanded` and `aria-controls`, and focus returns to it when the drawer closes. It renders nothing in a shell without a sidebar.| Prop | Type | Default | Description |
|---|---|---|---|
aria-label | string | 'Open navigation' | The button’s accessible name. |
size | 'sm' | 'md' | 'lg' | 'md' | Button size. |
useAppShell()
The drawer state, `{ sidebarOpen, setSidebarOpen }`, for your own menu button or to close the drawer after navigating.Accessibility
- Web landmarks: the header is a banner, the sidebar a navigation landmark named by its
aria-label, Main ismainand the footer is contentinfo. - The drawer is a Drawer: a dialog named after the sidebar, with focus trapped inside while open and returned to the trigger on close. Escape, the overlay and its × button close it.
- The scrolling area takes keyboard focus (web), so arrow keys and Page Down scroll Main without a mouse.
- The desktop sidebar stays in the document below the breakpoint, hidden with CSS, so it is out of the accessibility tree there and the drawer is the only copy screen readers meet.
| Key | Action |
|---|---|
| Enter / Space | On the trigger: opens the drawer. |
| Tab / Shift+Tab | Cycles focus inside the open drawer. |
| Escape | Closes the drawer. |
Responsive behavior
Below sidebarBreakpoint the sidebar moves into a drawer and the trigger appears. On web that is CSS media queries, so server-rendered HTML is right at every width with no flash; on iOS and Android it follows the window size. A drawer left open closes when the window widens past the breakpoint.
Platform notes
Web
The shell is
100dvh tall and only Main scrolls, inside the shell, so the header, sidebar and footer need no sticky positioning. The desktop sidebar scrolls on its own when it is taller than the screen.iOS
The shell is
flex: 1 and Main is a ScrollView. The header pads the top safe area, the footer the bottom one, and the drawer every edge it touches; the insets come from UniversalProvider. With avoidKeyboard (since 0.15.0) Main insets its content by the keyboard and scrolls the focused field into view; the footer stays under the keyboard.Android
The shell is
flex: 1 and Main is a ScrollView. The back button closes an open drawer. Safe areas are padded as on iOS. Edge-to-edge apps no longer shrink for the keyboard, so with avoidKeyboard (since 0.15.0) Main ends where the keyboard starts, measured on screen, so a navigation header above needs no offset; Android then scrolls the focused field into view.