Skip to main content

Full-Screen Wizard

A full-screen onboarding wizard with a persistent progress bar in the chrome, a mix of component and element-returning loaders, and a final resolve that returns the collected data. Step changes play a Motion crossfade-slide — motion.div wrappers driven by the outlet's transition prop.

The first transition uses a normal component loader. The final transition simulates async work before returning JSX with hydrated props. The progress bar updates immediately on advance — before the async step resolves — because chrome renders outside Suspense.

Click Start Onboarding below to try it.

Result
Loading...
Live Editor

Transition with Motion​

The outlet's transition prop receives { previousStep, nextStep, phase, onExited, transitionKey } and returns whatever should render in place of the step slot. The wizard returns two motion.div wrappers — the outgoing step fades out to the left while the incoming step fades in from the right:

The exit wrapper reports completion through onAnimationComplete={onExited}. That single call advances the outlet's phase machine from exiting → entering → exited, unmounting the outgoing step when its Motion animation finishes.

Async step loading​

The final step waits on a simulated async lookup and then returns JSX with hydrated props:

advance(async () => {
const summary = await loadWizardSummary();
return <StepConfirm summary={summary} />;
});

advance(loader, newContext) accepts a step loader function. That loader can synchronously return a component or asynchronously return a promise. While the loader is pending, the <Suspense> fallback inside WizardChrome shows the loading indicator.

This uses a timeout to mimic a fetch or server-side decision rather than a lazy import, which is often the more practical case for passing props into the next step.

Progress via context​

The progress bar reads ctx.step and ctx.total from useSequentContext(). Each advance call passes updated context:

advance(() => StepPreferences, { step: 2, total: 3, data: { name: "…" } })

Because chrome renders outside the Suspense boundary, the progress bar jumps from 33% → 66% immediately when advance is called — before the next step finishes loading.

Key points​

  • Any animation library works — Motion is used here as an example; since the slot just owns rendering, you could drive step transitions with Motion, Framer Motion, CSS transitions, or any other animation library you prefer.
  • Chrome outlives async loads — the progress bar and step counter stay mounted while the next step resolves.
  • Context still carries shared flow state — progress metadata stays in context while the final step also receives a hydrated summary prop.
  • No centralized step list — the total step count is just data passed through context; you can change it without touching any config.