Components
Header
One component, four composable rows, ten scroll behaviors. Rows only render when you pass their props, so the same Header scales from a single slim bar to a full nav + title + search stack with a mobile menu.
The four rows
Each row appears only when its props are provided, top to bottom: the nav row (logo on the left, nav links beside it — hidden below md — and actions on the right), the context row (title and subtitle), the search row (searchContent — an input, filters, or a ScrollNav), and the mobile menu panel (mobileMenu), an accordion that the hamburger button toggles on small screens.
import { AppShell, Header, Content } from "appshell-react";
import { Bell } from "lucide-react";
export default function App() {
return (
<AppShell safeArea>
<Header
behavior="fixed"
logo={<span className="font-bold">Atlas</span>}
actions={
<button aria-label="Notifications">
<Bell className="size-5" />
</button>
}
/>
<Content className="p-4">{/* itinerary */}</Content>
</AppShell>
);
}Behaviors
The behavior prop picks one of ten scroll strategies. In every reveal-* behavior the slim nav row stays sticky at the top while the heavier context and search rows scroll away with the page; the moment you scroll up, the behavior’s named rows glide back as a fixed overlay (and onVisibilityChange fires as the overlay shows and hides).
| Behavior | What happens |
|---|---|
| static | Rendered in the page flow — the whole header scrolls away with the content and comes back at the top. |
| fixed | The whole header (all rows) pins to the top of the viewport; content scrolls beneath it. |
| sticky | Pins the whole header exactly like fixed — the two values currently render identically and are kept as separate names for future divergence. |
| reveal-all | Nav row stays pinned while the context and search rows scroll away; scrolling up brings all rows back as a fixed overlay. |
| reveal-nav | Same pinned nav row; on scroll up, only the nav row returns in the overlay (it floats over the page with a shadow). |
| reveal-context | On scroll up, only the context (title/subtitle) row returns. |
| reveal-search | On scroll up, only the search row returns — discovery stays one gesture away. |
| reveal-nav-context | On scroll up, the nav and context rows return together. |
| reveal-nav-search | On scroll up, the nav and search rows return together. |
| reveal-context-search | On scroll up, the context and search rows return together. |
import { AppShell, Header, Content, ScrollNav, ScrollNavItem } from "appshell-react";
export default function App() {
return (
<AppShell safeArea>
<Header
behavior="reveal-all"
logo={<span className="font-bold">Pulse</span>}
title="Your feed"
subtitle="34 new posts since this morning"
searchContent={
<ScrollNav>
<ScrollNavItem label="All" active />
<ScrollNavItem label="Following" />
<ScrollNavItem label="Trending" />
</ScrollNav>
}
mobileMenu={
<nav className="flex flex-col gap-1">
<a className="rounded-md px-3 py-2 hover:bg-accent" href="/feed">
Feed
</a>
<a className="rounded-md px-3 py-2 hover:bg-accent" href="/messages">
Messages
</a>
</nav>
}
/>
<Content className="p-4">{/* posts */}</Content>
</AppShell>
);
}Themes and speed
theme styles all rows at once: "light" (your background/foreground tokens, the default), "primary" (your primary tokens), "dark" (a fixed near-black zinc palette), and "none" (zero styles — bring your own). Custom row content can adapt via useHeaderTheme(); see Theming. speed sets the duration preset for the reveal overlay and mobile menu animations: "slow" (0.6s), "normal" (0.3s), "fast" (0.15s).
The --header-height variable
Header measures itself with a ResizeObserver and writes --header-height (in pixels) to the root element, keeping it in sync as rows appear or wrap. Anything that should dock below the header — sticky tab strips, in-page anchors, a docked Sidebar — can use top: var(--header-height). See the sticky sub-navigation example. Never author the variable statically in CSS — the Header overwrites it on mount.
API
| Prop | Type | Default | Description |
|---|---|---|---|
| behavior | HeaderBehavior | "fixed" | "static" | "fixed" | "sticky" | "reveal-all" | "reveal-nav" | "reveal-context" | "reveal-search" | "reveal-nav-context" | "reveal-nav-search" | "reveal-context-search". |
| theme | "light" | "primary" | "dark" | "none" | "light" | Visual theme for all rows. "none" ships zero styles — bring your own via className. |
| speed | "slow" | "normal" | "fast" | "normal" | Duration preset for reveal and menu animations. |
| logo | ReactNode | — | Brand element on the left of the nav row. |
| nav | ReactNode | — | Navigation links (typically <HeaderNav>). Hidden below md, where the hamburger + mobileMenu take over. |
| actions | ReactNode | — | Right-aligned controls on the nav row. |
| title | ReactNode | — | Context row heading. |
| subtitle | ReactNode | — | Context row supporting line. |
| searchContent | ReactNode | — | Search row content — an input, filters, or a <ScrollNav>. |
| rowOrder | ("context" | "search")[] | ["context", "search"] | Order of the two lower rows. Flip it to put the search row directly under the nav row, above the title block. Reveal thresholds follow whatever order you set. |
| mobileMenu | ReactNode | — | Content for the animated mobile menu panel. Providing it renders the hamburger button below md. |
| onVisibilityChange | (visible: boolean) => void | — | Fires when a reveal behavior shows or hides the overlay. |
| forceSafeAreaTop | boolean | false | Keep the top safe-area padding on non-pinned behaviors. AppShell sets this automatically for pinned headers. |
| className | string | — | Extra classes for the header wrapper. |