Component
StackNavigator
StackNavigator animates a consumer-owned list of views while preserving covered DOM state, scroll, and focus. It never reads a URL, writes history, or chooses what to navigate to.
Preview
Installation
pnpm dlx shadcn@latest add https://pwaui.com/r/stack-navigator.jsonUsage
"use client"
import * as React from "react"import { NavigationBar } from "@/components/ui/navigation-bar"import { StackNavigator, useStackNavigator } from "@/components/ui/stack-navigator"
function ProjectDetail() { const { pop } = useStackNavigator()
return ( <> <NavigationBar> <NavigationBar.Leading> <NavigationBar.BackButton data-autofocus onClick={pop} /> </NavigationBar.Leading> <NavigationBar.Title>Project</NavigationBar.Title> </NavigationBar> <main>Project detail</main> </> )}
export function ProjectStack() { const [detailOpen, setDetailOpen] = React.useState(false) const entries = [ { key: "projects", label: "Projects", content: <button onClick={() => setDetailOpen(true)}>Open project</button> }, ...(detailOpen ? [{ key: "project", label: "Project detail", content: <ProjectDetail /> }] : []), ]
return <StackNavigator backGesture="auto" entries={entries} onPop={() => setDetailOpen(false)} />}Anatomy
StackNavigatorstack viewedge-swipe layeruseStackNavigatorentriesonPopComposition
See how PWAProvider, AppShell, SafeArea, NavigationBar, and TabBar divide viewport, scrolling, safe-area, and navigation responsibilities.
Behavior notes
- entries is the complete controlled stack. Append to push and remove from the end to pop; the component keeps no private navigation history.
- Covered views stay mounted but inert and aria-hidden, preserving uncontrolled input state and scroll without exposing hidden controls to keyboard or assistive-technology navigation.
- useStackNavigator exposes depth, canPop, and pop so a NavigationBar.BackButton inside the top entry can request onPop.
- backGesture defaults to auto: it enables only in standalone or fullscreen display mode. Use on only on a surface where you have verified that the browser or operating system does not own the leading edge; use off to disable it completely.
- backGestureEdgeWidth defaults to 24 pixels and backGestureThreshold defaults to half the stack width. Tracking publishes --pwa-stack-swipe-progress and data-back-gesture-state without rerendering React on pointer moves.
- onDepthChange can hide a persistent TabBar or update other shell chrome without querying the DOM.
- Keep stacks shallow enough that retaining covered DOM remains inexpensive. Each view owns its own scroll viewport.
Platform limitations
- Same-document View Transitions are a progressive enhancement. StackNavigator feature-detects the API and uses its transform/opacity fallback when unavailable; reduced-motion requests bypass sliding in either path.
- Safari browser tabs already reserve the screen edge for browser history, so auto deliberately disables the custom gesture there. Installed iOS and Android behavior still requires device QA; auto is a conservative policy, not proof that every OS configuration leaves the edge free.
- Android system Back does not arrive as this pointer gesture. Route hardware or system back through your router, then derive entries from that route state.
- The gesture currently starts from the physical left edge. RTL leading-edge mirroring is a known follow-up; set backGesture to off for RTL surfaces that require a right-edge gesture.
- The component cannot synchronize deep links or browser back on its own. When a router owns entries, map router-rendered state into the controlled array and route onPop back through that router.
Transition support
| Platform | Path | Notes |
|---|---|---|
| Chrome / Android WebView | Native enhancement | Same-document View Transitions from Chrome 111; older engines use the CSS fallback. |
| Safari / iOS | Native enhancement | Same-document View Transitions from Safari 18; older versions use the CSS fallback. |
| Firefox | Native enhancement | Same-document View Transitions from Firefox 139; older versions use the CSS fallback. |
Accessibility
Only the active view is reachable. Push focuses an autofocus target or the new view container; pop returns focus to the pointer or keyboard trigger when it remains mounted, otherwise to the revealed view.