Content Header

Breadcrumbs, a title, actions, and docked tabs that stay in sync as you move between screens.

<ContentHeader breadcrumbs> + <Tabs>content-headerbreadcrumbstabs
Open fullscreen
9:41

In this demo

Bramble — project tracker

What to try

  • Click a breadcrumb to jump between screens
  • Switch tabs — the row stays docked under the header

Code

app.tsx
import { AppShell, Header, Content, ContentHeader, Breadcrumbs, BreadcrumbItem, Tabs, Tab } from "appshell-react";
import { useState } from "react";

export default function App() {
  // Breadcrumb clicks just move between in-page "screens" — no router needed.
  const [screen, setScreen] = useState<"workspace" | "projects" | "project">("project");
  const [tab, setTab] = useState("tasks");

  return (
    <AppShell safeArea>
      <Header behavior="sticky" logo={<span className="font-bold">Bramble</span>} />

      <Content className="mx-auto max-w-2xl">
        <ContentHeader
          breadcrumbs={
            <Breadcrumbs>
              <BreadcrumbItem label="Bramble" onClick={() => setScreen("workspace")} current={screen === "workspace"} />
              {screen !== "workspace" && (
                <BreadcrumbItem label="Projects" onClick={() => setScreen("projects")} current={screen === "projects"} />
              )}
              {screen === "project" && <BreadcrumbItem label="Website redesign" current />}
            </Breadcrumbs>
          }
          title="Website redesign"
          subtitle="12 open tasks · due Oct 3"
          actions={<button className="rounded-lg bg-primary px-3 py-2 text-sm font-medium text-primary-foreground">New task</button>}
        />

        {/* Tabs dock below the Header via --header-height automatically. */}
        <Tabs value={tab} onValueChange={setTab}>
          <Tab value="tasks" label="Tasks" />
          <Tab value="milestones" label="Milestones" />
          <Tab value="files" label="Files" />
        </Tabs>

        {tab === "tasks" && <TaskList />}
      </Content>
    </AppShell>
  );
}