Pages & Navigation
Build multi-page paywalls, onboardings, and funnels with file-based pages and a stack router — no network between steps, no loading spinners.
Multi-page paywalls, onboarding quizzes, and funnels are built from file-based pages and a stack router. Moving between pages never touches the network — the whole flow ships together, so there's no page load, no spinner, and no screen that never arrives.
Add pages
Every .tsx file in app/ is a page; directories nest the name:
app/
├── index.tsx "index" — every flow starts here
├── plans.tsx "plans"
├── layout.tsx wraps every page (the one reserved name)
└── goals/
├── index.tsx "goals"
└── setup.tsx "goals/setup"File names are lowercase-kebab, and each page default-exports a component. Components that aren't pages go in components/, not app/ — a stray file there is a warning in dev and blocks a push.
Only the top-level layout.tsx is special. A nested goals/layout.tsx would become a page named goals/layout — there are no nested layouts.
Navigate
import { useRouter } from "superwall/navigation";
const router = useRouter();
router.push("goals/setup"); // forward
router.push("plans", { transition: "fade" }); // with a transition
router.replace("terms"); // swap the current page
router.back(); // one step back
router.canGoBack(); // anything to go back to?
router.dismiss(2); // back two steps
router.dismissAll(); // back to the first page
router.dismissTo("goals"); // unwind to a page in the stack
router.name; // current page
router.depth; // pages underneath (index = 0)If you've used expo-router, this is its API, method for method. Page names autocomplete and reject typos, thanks to the generated superwall.d.ts — one more reason to commit it.
A few rules make navigation feel right:
- Closing the paywall is
useActions().close(), not navigation. The stack is for moving within the flow; closing hands control back to your app. See Actions. - Fire
haptics.light()before every push and back. iOS gives no feedback of its own on navigation inside a paywall. - Pages you navigate away from stay alive. Going back restores a page exactly as it was left, scroll position and state included. A covered page can't be clicked or focused;
useIsFocused()tells a page it's covered so it can pause video or timers. - There is no declared page order. Any page can push any page — which is exactly what makes branching flows possible.
- Page views are tracked for you. Every navigation reports analytics automatically; there's nothing to instrument.
Pass state between pages
Navigation carries no params, on purpose. Cross-page state has two homes:
layout.tsx stays mounted for the whole flow — React state or context there is visible to every page:
export default function Layout({ children }: PropsWithChildren) {
return <div className="shell"><Chrome />{children}</div>;
}A plain module works even after the collecting page is gone — the quiz pattern, from the onboarding quiz example (see Examples):
// components/answers.ts
export const answers: { goal?: Goal; level?: Level } = {};const choose = (value: Goal) => {
haptics.selection();
answers.goal = value;
router.push("level");
};Guard every read on the destination — answers.goal ? PLAN[answers.goal] : undefined — so a revisited page never crashes on a missing answer.
Shared chrome
Put back buttons, step counters, and the close button in layout.tsx, and drive them from router state so they can never drift from the stack:
const router = useRouter();
{router.canGoBack()
? <button onClick={() => { haptics.light(); router.back(); }}>Back</button>
: <span className="chrome-button" />} /* placeholder keeps the layout stable */
<span>{router.depth + 1} of 3</span>depth + 1 works as a step counter only in linear flows. In a branching flow, a page's depth isn't its step number — label steps per page instead.
When the layout wraps chrome around the pages, set two variables in :root:
:root {
--sw-background: var(--bg); /* pages are opaque; give them your background */
--sw-routes-height: auto; /* let the layout own the height, or its footer is pushed off-screen */
}Position overlay chrome absolutely over the pages rather than as a bar above them — each page paints its own background, so a bar of its own shows as a seam during transitions.
A funnel is one paywall, not several
Multi-step flows — onboarding quizzes, web funnels — are one paywall whose steps are pages, not a chain of separate paywalls. Every step is a router.push in the same flow, so there's no load between steps and nothing to re-fetch. The structure is identical — config.ts plus app/ pages plus layout.tsx — and funnels live in superwall/funnels/<id>/ with exactly the same shape.
The web funnel example is the reference: question steps, a typed plan selector, then purchase(reference) at the end — with web checkout taking payment in the same flow.
Where transitions and animation fit
How pages move — the built-in transitions, custom ones, and bottom sheets — is covered in Transitions. Animation inside a page (Motion, CSS) is yours; moving between pages stays the router's job. Keeping that line means spamming navigation can never fight your component animations. And entry animations gate on presentation, never mount — see Lifecycle & events.
Assets for upcoming pages preload automatically while the user is on the current page — see Assets.
How is this guide?