Skip to content

Components

AppShell

The root wrapper every shell starts with. It provides the shared context the other components coordinate through, orchestrates safe-area padding, and places Header and docked Sidebar children into the layout for you.

What it does

AppShell renders a full-height (min-h-dvh) flex column and wraps it in the shell context — the store behind useAppShell() that tracks headerVisible, footerVisible, and the current scroll direction. On top of that it does two structural jobs: applying safe-area padding in the right place for the header behavior in use, and building the column (or two-column) layout from your children.

Open fullscreen
9:41
app.tsx
import { AppShell, Header, Content, Footer, FooterItem } from "appshell-react";
import { Home, Search, User } from "lucide-react";

export default function App() {
  return (
    <AppShell safeArea>
      <Header
        behavior="fixed"
        logo={<span className="font-bold">Feedflow</span>}
        title="Timeline"
      />
      <Content className="p-4">{/* page content */}</Content>
      <Footer variant="tab-bar" behavior="auto-hide">
        <FooterItem icon={<Home />} label="Home" active />
        <FooterItem icon={<Search />} label="Search" badge={3} />
        <FooterItem icon={<User />} label="Profile" />
      </Footer>
    </AppShell>
  );
}

Child placement

AppShell inspects its direct children by component type. A direct <Header> child is lifted to the top of the shell, and any direct <Sidebar variant="docked"> children are pulled into a horizontal row beside a content column — that is how the two-column docked layout appears without any wrapper markup from you. Everything else flows into the content column in the order you wrote it.

A docked sidebar in that row sticks below the header when the header is pinned (fixed or sticky, via the --header-height variable); when the header scrolls away (static or any reveal-* behavior) or there is no header, it sticks to the top of the viewport instead.

Detection matches the component type (or a displayName of "Header"/"Sidebar"), so it only works for direct children. If you wrap Header in your own component, AppShell treats it as ordinary content — it still renders, but it loses the automatic placement and the safe-area coordination described below. Overlay Sidebars (the default variant) are not hoisted; they position themselves.

Safe areas

With safeArea enabled, what happens depends on the header behavior. A behavior="static" header must scroll away with the page, so the whole shell — header included — sits inside a top-and-bottom padded container. For fixed, sticky, and the reveal-* behaviors, the header manages its own top inset instead: AppShell passes it forceSafeAreaTop so the pinned bar extends behind the notch, and only the content column gets bottom padding. Without safeArea (and with no docked sidebar), AppShell is just the flex column and renders children untouched.

Inset values come from the platform standard env(safe-area-inset-*) (requires viewport-fit=cover in your viewport meta tag) — AppShell never invents them. Mockups and tests can simulate insets by defining --appshell-safe-area-inset-* overrides. Details in Theming.

API

PropTypeDefaultDescription
safeAreabooleanfalsePad the shell for device safe areas (notch, home indicator). Static headers stay inside the padded scroll container; pinned headers manage their own top inset.
skipToContentbooleanfalseRender a skip link as the first focusable thing on the page, jumping past the header and navigation to Content. Visible only while focused.
routeKeystring—Something that changes on navigation — a pathname is the usual choice. When it changes, focus moves to the top of the new screen so a screen reader announces it instead of the keyboard being left in the link the user just used.
focusOnRouteChange"heading" | "content" | false"heading"Where that focus lands. "heading" targets the Header title and falls back to Content when a screen has none.
classNamestring—Extra classes for the root element.
children*ReactNode—Shell regions. A direct Header child and any direct <Sidebar variant="docked"> children are detected by type and placed automatically; everything else flows into the content column.