Homechevron_rightBlogchevron_rightHow to Build a SaaS Onboarding Flow in Next.js

Tutorial

How to Build a SaaS Onboarding Flow in Next.js

A step-by-step guide to building a polished SaaS onboarding flow in Next.js — welcome screen, feature highlights, profile setup, goal selection, team invite and connect integrations.

15 April 2026·8 min read

Your onboarding flow is the first real product experience every new user has. Research consistently shows that users who reach their “aha moment” — the first time your product delivers clear value — within their first session are dramatically more likely to retain and convert. Poor onboarding means paying for acquisition you can’t recoup.

The mistake most teams make is treating onboarding as an afterthought. They build the core product, bolt on a few modals, and ship it. Then they wonder why 60–70% of trial signups never come back. This guide walks you through building a proper multi-step onboarding flow in Next.js: which screens to include, how to structure the components, and the UX principles that separate flows users complete from flows they abandon.

What a complete SaaS onboarding flow looks like

A well-structured flow typically has five to eight steps:

  1. Welcome — confirm they’re in the right place, set expectations
  2. Feature highlights — show the three or four core things your product does
  3. Profile setup — collect essential personalisation data
  4. Choose a goal — understand what they’re trying to achieve
  5. Workspace setup — create their first project or team
  6. Invite team — get colleagues in early (critical for B2B activation)
  7. Connect integrations — hook up the tools they already use
  8. Setup complete — celebrate, confirm what’s next

Not every product needs all eight. A solo tool can skip team invite. A single-tenant app might skip workspace setup. The principle is: collect only what you need to deliver value, in the right order.

Step 1: Tracking progress

Before building any screen, decide how you’ll track state. The simplest approach uses React state for the current step index and total steps count:

const [step, setStep] = useState(1)
const totalSteps = 6

// percentage for the progress bar
const pct = totalSteps > 0 ? Math.min(100, Math.round((step / totalSteps) * 100)) : 0

A ProgressStripcomponent renders a thin bar at the top of every screen — a visual anchor that tells the user exactly how far they’ve come and how little is left. Always clamp to 100 and guard against zero totalSteps to avoid NaN in the width style.

function ProgressStrip({ step, totalSteps, stepLabel }: ProgressStripProps) {
  const pct = totalSteps > 0 ? Math.min(100, Math.round((step / totalSteps) * 100)) : 0
  return (
    <div className="w-full mb-6">
      <div className="flex justify-between text-xs text-gray-400 mb-1">
        <span>{stepLabel}</span>
        <span>{step} of {totalSteps}</span>
      </div>
      <div className="h-1.5 bg-gray-100 rounded-full overflow-hidden">
        <div
          className="h-full bg-indigo-500 rounded-full transition-all duration-500"
          style={{ width: `${pct}%` }}
        />
      </div>
    </div>
  )
}

Step 2: Welcome screen

The welcome screen does one job: confirm to the user they’re in the right place and make them feel good about signing up. Keep copy tight. Show your product logo, a headline, and a single CTA.

interface WelcomeProps {
  userName?: string
  productName: string
  tagline: string
  ctaLabel?: string
  onStart: () => void | Promise<void>
}

Don’t ask for anything on this screen. It’s a handshake — “you made it, here’s what happens next.” Any data collection here will drop your completion rate.

Step 3: Feature highlights

Use this screen to frame the three or four most important things your product does. This is not a feature list — it’s a curated “here’s how this changes your work” pitch. Keep each description to a single sentence. If you’re writing two, your list is too long.

interface Feature {
  icon: string      // e.g. Material Symbols name
  title: string
  description: string
}

interface FeatureHighlightsProps {
  step: number
  totalSteps: number
  stepLabel: string
  headline: string
  features: Feature[]
  onNext: () => void | Promise<void>
}

Step 4: Profile setup

Collect the minimum personal data needed for personalisation: display name, optionally a job title, optionally an avatar. Every additional field reduces completion rates — only ask for data you immediately use. Validate only what is strictly required.

interface ProfileData {
  firstName: string     // required
  lastName?: string
  jobTitle?: string
  avatar?: File
}

interface ProfileSetupProps {
  step: number
  totalSteps: number
  stepLabel: string
  onSave: (data: ProfileData) => Promise<void>
}

Set aria-invalid={false} on optional fields rather than tying invalid state to form submission — it avoids incorrectly marking blank optional inputs as erroneous on first render.

Step 5: Goal selection

Understanding why someone signed up tells you how to activate them. Present three to six goals as selectable cards. The selected goal feeds into which features you surface first once onboarding completes.

interface Goal {
  id: string
  icon: string
  label: string
  description: string
}

interface ChooseGoalProps {
  step: number
  totalSteps: number
  stepLabel: string
  goals: Goal[]
  onSelect: (goalId: string) => Promise<void>
}

Pass a loading state down to each goal card so they become disabled while theonSelect promise is resolving — this prevents double-submission. Reset form state to "idle" after the promise resolves successfully before advancing.

Step 6: Invite team

For B2B SaaS, getting a second team member in during onboarding is one of the strongest retention signals you can measure. The invite step collects email addresses and dispatches invites. Always include a “Skip for now” path — forcing an invite blocks solo evaluators and costs you completions.

interface Invite {
  email: string
  role?: string
}

interface InviteTeamProps {
  step: number
  totalSteps: number
  stepLabel: string
  roleOptions?: string[]
  maxInvites?: number
  onSend: (invites: Invite[]) => Promise<void>
  onSkip: () => void
}

Step 7: Connect integrations

Show relevant integrations as cards with individual Connect buttons. Each integration needs its own loading and success state — connecting Slack shouldn’t grey out the whole screen while you’re waiting for an OAuth callback.

type FormState = "idle" | "loading" | "success" | "error"

// Track per-integration state
const [states, setStates] = useState<Record<string, FormState>>({})

async function handleConnect(id: string) {
  setStates(s => ({ ...s, [id]: "loading" }))
  try {
    await onConnect(id)
    setStates(s => ({ ...s, [id]: "success" }))
  } catch {
    setStates(s => ({ ...s, [id]: "error" }))
  }
}

Step 8: Setup complete

Close the loop with a celebration screen. Mirror back a checklist of what the user configured (“Profile created”, “Goal set”, “3 invites sent”), optionally fire confetti, and give them a clear CTA to their first meaningful action in the product.

interface SetupCompleteProps {
  userName?: string
  completedSteps: string[]
  ctaLabel: string
  ctaHref?: string
  onComplete: () => void | Promise<void>
}

UX principles that move activation

One decision per screen. If a step asks users to set up a profile and pick a goal, completion rates drop. Each screen should have exactly one primary question or action.

Always provide a skip path.Any step that doesn’t directly improve the product’s ability to serve the user should have a skip option. Users who skip often return once they’ve seen value.

Show optimistic progress.Advance the progress bar the moment the user clicks Next, before async operations resolve. Perceived wait time drops significantly when users can see they’ve already moved forward.

Preserve state on back navigation. If a user goes back from step four to step three, their data should still be there. Losing form data on back navigation is one of the top frustration points in onboarding.

Keep the right col sticky. On wider screens, show the progress strip or a summary panel in a sticky sidebar while the main form scrolls. This gives users a constant sense of where they are.


Decidr

Skip the build — use the Onboarding Pages Pack

Every component described in this guide — Welcome, Feature Highlights, Profile Setup, Choose Goal, Workspace Setup, Invite Team, Connect App, and Setup Complete — is included in the Decidr Onboarding Pages Pack. Eight fully typed React components, three layout variants each, zero UI library dependencies. Copy one file into your project and start customising.

View the Onboarding Pages Packarrow_forward