In-Page Navigation

Anchor navigation with scroll-spy highlighting, docked below a reveal-nav header.

behavior="reveal-nav" + anchorsanchorsscroll-spytoc
Open fullscreen
9:41

In this demo

Handbook — field guide

What to try

  • Scroll — the active section follows you
  • Jump between sections from the pill row

Code

app.tsx
import { AppShell, Header, Content, MotionProvider } from "appshell-react";
import { framerMotionAdapter } from "appshell-react/motion-framer";

const sections = ["basics", "camp-setup", "knots", "weather", "first-aid"];

export default function App() {
  const [active, setActive] = useState(sections[0]);
  // An IntersectionObserver (rootMargin "-120px 0px -65% 0px") sets 'active'.

  const jump = (id) =>
    document.getElementById(id)?.scrollIntoView({ behavior: "smooth" });

  return (
    <MotionProvider adapter={framerMotionAdapter}>
      <AppShell safeArea>
        <Header behavior="reveal-nav" logo={<span>Handbook</span>} title="The Overnight" />
        {/* Anchor pills dock below the visible header, never above the safe area */}
        <div
          style={{ top: "max(var(--header-height), var(--appshell-safe-area-inset-top, env(safe-area-inset-top)))" }}
          className="sticky z-40 border-b bg-background/95 backdrop-blur"
        >
          <nav className="flex gap-2 overflow-x-auto px-4 py-2.5">
            {sections.map((id) => (
              <button key={id} onClick={() => jump(id)} data-active={active === id}>
                {id}
              </button>
            ))}
          </nav>
        </div>
        {/* Sections carry scroll-mt-28 so anchors land below both bars */}
        <Content>{/* <section id="basics" className="scroll-mt-28">…</section> */}</Content>
      </AppShell>
    </MotionProvider>
  );
}