Skip to content

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.

Open fullscreen
9:41
app.tsx
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).

BehaviorWhat happens
staticRendered in the page flow — the whole header scrolls away with the content and comes back at the top.
fixedThe whole header (all rows) pins to the top of the viewport; content scrolls beneath it.
stickyPins the whole header exactly like fixed — the two values currently render identically and are kept as separate names for future divergence.
reveal-allNav row stays pinned while the context and search rows scroll away; scrolling up brings all rows back as a fixed overlay.
reveal-navSame pinned nav row; on scroll up, only the nav row returns in the overlay (it floats over the page with a shadow).
reveal-contextOn scroll up, only the context (title/subtitle) row returns.
reveal-searchOn scroll up, only the search row returns — discovery stays one gesture away.
reveal-nav-contextOn scroll up, the nav and context rows return together.
reveal-nav-searchOn scroll up, the nav and search rows return together.
reveal-context-searchOn scroll up, the context and search rows return together.
Open fullscreen
9:41
app.tsx
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).

Mobile menu

Desktop nav links hide below the md breakpoint, and mobileMenu is the small-screen counterpart: providing it renders a hamburger button next to the logo (below md only), which toggles an animated accordion panel under the header rows containing whatever you pass — typically a vertical link list. Without mobileMenu, no hamburger renders.

The panel opens and closes only through the hamburger button — it does not auto-close when a link inside is tapped, so on client-side-routed apps close it yourself (or let the navigation unmount the header).

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

PropTypeDefaultDescription
behaviorHeaderBehavior"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.
logoReactNode—Brand element on the left of the nav row.
navReactNode—Navigation links (typically <HeaderNav>). Hidden below md, where the hamburger + mobileMenu take over.
actionsReactNode—Right-aligned controls on the nav row.
titleReactNode—Context row heading.
subtitleReactNode—Context row supporting line.
searchContentReactNode—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.
mobileMenuReactNode—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.
forceSafeAreaTopbooleanfalseKeep the top safe-area padding on non-pinned behaviors. AppShell sets this automatically for pinned headers.
classNamestring—Extra classes for the header wrapper.