EmpireUI
Get Pro
← Blog9 min read#glassmorphism#onboarding#react

Glassmorphism Onboarding UI: Multi-Step Wizard With Frosted Steps

Build a multi-step onboarding wizard with frosted-glass steps in React — smooth transitions, accessible progress, and real Tailwind code you can ship today.

frosted glass multi-step wizard UI on gradient background

Why Onboarding Wizards and Glassmorphism Are a Natural Pair

Onboarding flows live or die on perceived progress. You're asking users to hand over personal details, choose settings, and commit before they've seen any value — that's a tough ask. A well-designed wizard breaks the ask into digestible steps, and glassmorphism's frosted, layered surfaces do something clever: they make each step feel like a distinct physical layer you're working through, which reinforces that sense of forward momentum.

Honestly, the combo works so well because both ideas are fundamentally about depth. The backdrop-filter: blur() effect creates visual hierarchy between the active step and the background. Users subconsciously read the blurred backdrop as "already handled" — it's behind you, literally. That's not a coincidence, it's perception design.

Worth noting: this pattern has exploded since 2024 in SaaS products, especially in auth flows and workspace-setup wizards. You'll see it in Notion, Linear, and a dozen funded startups all pulling from the same playbook. The good news is it's not hard to build yourself — and you don't need a design system budget to pull it off. Empire UI's glassmorphism components give you the primitives for free.

Before you write a line of code, sketch out your step count. Three to five steps is the sweet spot. More than that and users start abandoning on step two, regardless of how beautiful the UI looks. Fewer than three and you probably don't need a wizard at all — just a single form will do.

The Component Architecture: Thinking in Layers

A multi-step wizard has three distinct UI layers: the progress indicator (where am I?), the step panel (what do I do now?), and the navigation controls (where do I go next?). Each of these gets its own glassmorphism treatment, but they shouldn't all scream at the same volume.

The step panel gets the primary glass treatment — backdrop-blur-xl, full border highlight, the works. The progress indicator sits above it, slightly less blurred (backdrop-blur-sm) so it reads as a meta-layer above the content. Navigation buttons get the most subtle treatment: bg-white/10 with a border but no blur, so they don't compete with the main panel.

Quick aside: don't blur the entire page. Pick a vivid gradient background — something like from-indigo-700 via-purple-600 to-pink-500 — and let the blur do its thing against that. If your background is too neutral, the frosted effect collapses into a grey smear. You can experiment with background-to-glass combos using the glassmorphism generator before committing to one.

State management for a wizard is dead simple. A single step integer in useState, a formData object that accumulates answers across steps, and a direction flag ('forward' | 'back') for driving the transition animation. That's it. You don't need Redux or Zustand here — the wizard's lifespan is a single page session.

Building the Glass Step Panel

Here's the core step panel component. It handles the frosted glass surface, the step content slot, and entry/exit animations via Framer Motion. If you're not using Framer Motion, a plain CSS transition on opacity and transform works fine too.

// GlassStepPanel.tsx
import { motion, AnimatePresence } from 'framer-motion';

interface GlassStepPanelProps {
  step: number;
  direction: 'forward' | 'back';
  children: React.ReactNode;
}

const variants = {
  enter: (dir: string) => ({
    x: dir === 'forward' ? 48 : -48,
    opacity: 0,
  }),
  center: { x: 0, opacity: 1 },
  exit: (dir: string) => ({
    x: dir === 'forward' ? -48 : 48,
    opacity: 0,
  }),
};

export function GlassStepPanel({ step, direction, children }: GlassStepPanelProps) {
  return (
    <div className="relative overflow-hidden rounded-3xl">
      {/* Frosted glass surface */}
      <div className="absolute inset-0 bg-white/10 backdrop-blur-xl border border-white/20 rounded-3xl" />
      <AnimatePresence mode="wait" custom={direction}>
        <motion.div
          key={step}
          custom={direction}
          variants={variants}
          initial="enter"
          animate="center"
          exit="exit"
          transition={{ type: 'spring', stiffness: 300, damping: 30 }}
          className="relative z-10 p-8"
        >
          {children}
        </motion.div>
      </AnimatePresence>
    </div>
  );
}

The absolute inset-0 trick is key here. By separating the glass surface div from the animated content div, you avoid the blur re-compositing on every animation frame. The surface stays still, only the content slides. That's a 40px savings in GPU workload on lower-end devices — not huge, but it matters.

One more thing — set overflow-hidden on the outer wrapper, not the glass surface. If you set it on the backdrop-blur element itself, some browsers clip the blur at the border radius and you get sharp corners on the blur effect. Outer container handles the clipping, inner surface handles the blur.

In practice, you'll want to test this on a real phone before you ship. The backdrop-blur-xl value (which maps to blur(24px)) is expensive on 2021-era Android mid-rangers. If you see jank, drop to backdrop-blur-md (12px) — the frosted effect survives and the frame rate recovers.

Progress Indicator With Frosted Steps

The progress bar is where you can get really creative. Instead of a flat progress line, a glassmorphism wizard calls for a step pill indicator — small frosted capsules that transition between inactive (dim glass) and active (bright glass with glow) states.

// StepIndicator.tsx
interface StepIndicatorProps {
  total: number;
  current: number;
  labels: string[];
}

export function StepIndicator({ total, current, labels }: StepIndicatorProps) {
  return (
    <div className="flex items-center gap-2 mb-8">
      {Array.from({ length: total }).map((_, i) => (
        <div key={i} className="flex items-center gap-2">
          <div
            className={[
              'flex items-center justify-center w-8 h-8 rounded-full text-xs font-semibold transition-all duration-300',
              i < current
                ? 'bg-white/30 border border-white/50 text-white shadow-[0_0_12px_rgba(255,255,255,0.4)]'
                : i === current
                ? 'bg-white/20 border border-white/40 text-white backdrop-blur-sm scale-110'
                : 'bg-white/5 border border-white/10 text-white/40',
            ].join(' ')}
          >
            {i < current ? (
              <svg className="w-4 h-4" viewBox="0 0 20 20" fill="currentColor">
                <path d="M16.7 5.3a1 1 0 010 1.4l-8 8a1 1 0 01-1.4 0l-4-4a1 1 0 111.4-1.4L8 12.58l7.3-7.3a1 1 0 011.4 0z" />
              </svg>
            ) : (
              i + 1
            )}
          </div>
          {i < total - 1 && (
            <div
              className={[
                'h-px w-8 transition-all duration-500',
                i < current ? 'bg-white/50' : 'bg-white/10',
              ].join(' ')}
            />
          )}
        </div>
      ))}
    </div>
  );
}

The shadow-[0_0_12px_rgba(255,255,255,0.4)] on completed steps gives a subtle glow that reads as "done" even without a label. It's a small detail but users notice it. That Tailwind arbitrary-value syntax for box shadows is one of those features that makes the library genuinely worth using for this kind of design work.

Add aria-current="step" to the active indicator and aria-label attributes with the step label on each pill. Screen readers don't see your glow effects, so make the semantic HTML do the work. Accessibility on wizard UIs is often an afterthought, but a role="progressbar" with aria-valuenow and aria-valuemax on the connector line goes a long way.

That said, don't show labels below every step pill if you have more than four steps — the indicator row gets cramped below 375px viewport width and the text wraps badly. Hide labels at sm: breakpoint and only show the number or icon in the pill itself.

Wiring Up the Full Wizard State Machine

Here's the full wizard shell. This is the piece that ties the step panel, indicator, and navigation controls together. Keep it simple — the complexity lives in individual step components, not the orchestrator.

// OnboardingWizard.tsx
import { useState } from 'react';
import { GlassStepPanel } from './GlassStepPanel';
import { StepIndicator } from './StepIndicator';
import { Step1Profile, Step2Preferences, Step3Integrations, Step4Done } from './steps';

const STEPS = ['Profile', 'Preferences', 'Integrations', 'Done'];
const STEP_COMPONENTS = [Step1Profile, Step2Preferences, Step3Integrations, Step4Done];

export function OnboardingWizard() {
  const [step, setStep] = useState(0);
  const [direction, setDirection] = useState<'forward' | 'back'>('forward');
  const [formData, setFormData] = useState({});

  const goNext = (stepData: Record<string, unknown>) => {
    setFormData(prev => ({ ...prev, ...stepData }));
    setDirection('forward');
    setStep(s => Math.min(s + 1, STEPS.length - 1));
  };

  const goBack = () => {
    setDirection('back');
    setStep(s => Math.max(s - 1, 0));
  };

  const ActiveStep = STEP_COMPONENTS[step];

  return (
    <div className="min-h-screen bg-gradient-to-br from-indigo-700 via-purple-600 to-pink-500 flex items-center justify-center p-6">
      <div className="w-full max-w-lg">
        <StepIndicator total={STEPS.length} current={step} labels={STEPS} />
        <GlassStepPanel step={step} direction={direction}>
          <ActiveStep
            formData={formData}
            onNext={goNext}
            onBack={goBack}
            isFirst={step === 0}
            isLast={step === STEPS.length - 1}
          />
        </GlassStepPanel>
      </div>
    </div>
  );
}

Each step component receives onNext, onBack, formData, isFirst, and isLast. The step validates its own fields and only calls onNext with the serialized data when it's happy. That keeps validation logic co-located with the fields it's validating — no global form schema to maintain.

Look, the temptation is to reach for a library like React Hook Form's multi-step wizard pattern or Formik's wizard plugin for this. Those are fine choices for complex forms. But for a 3-5 step onboarding flow, plain useState and a callback prop are genuinely all you need. The wizard re-renders are cheap, the state is shallow, and you avoid a 14KB dependency for something you could write in 40 lines.

One thing worth adding is URL-driven step state. Replace useState with a URL search param (?step=2) so users can share or bookmark mid-flow, and browser back/forward buttons work naturally. Next.js useSearchParams + router.push makes this a 10-minute refactor.

Navigation Buttons and the Glass Button Pattern

Navigation at the bottom of the step panel needs to feel deliberate without competing with the step content. The pattern that works: a "Back" button that's barely there (ghost, text-white/60), and a "Next" button that's the clearest thing on the panel (bg-white/20 border border-white/40 hover:bg-white/30).

// WizardNav.tsx
interface WizardNavProps {
  onNext: () => void;
  onBack: () => void;
  isFirst: boolean;
  isLast: boolean;
  nextLabel?: string;
}

export function WizardNav({ onNext, onBack, isFirst, isLast, nextLabel = 'Continue' }: WizardNavProps) {
  return (
    <div className="flex items-center justify-between mt-8 pt-6 border-t border-white/10">
      <button
        onClick={onBack}
        disabled={isFirst}
        className="px-4 py-2 text-sm text-white/60 hover:text-white/90 disabled:opacity-0 transition-all"
      >
        Back
      </button>
      <button
        onClick={onNext}
        className="
          px-6 py-2.5 rounded-xl text-sm font-medium text-white
          bg-white/20 border border-white/30
          hover:bg-white/30 hover:border-white/50
          active:scale-95
          transition-all duration-150
          shadow-[0_2px_8px_rgba(0,0,0,0.2)]
        "
      >
        {isLast ? 'Get Started' : nextLabel}
      </button>
    </div>
  );
}

The active:scale-95 on the Next button gives a press-down feel that works really well against the glass surface. It's a 1-line addition that makes the button feel physical. Pair this with the glass button variants you'll find in the Empire UI component library — they ship with the hover glow and active press already tuned.

Disable the Next button during async operations (submitting the final step, for instance) and show a spinner inside the button rather than a separate loading overlay. Covering the whole panel with a loader during that last-step submission breaks the layering and looks wrong with glassmorphism. Keep the glass visible, just lock the button.

Worth noting: on mobile, 44px minimum tap targets. The py-2.5 on a text-sm button gets you to roughly 38px height — not quite there. Add min-h-[44px] to both buttons to cover that. WCAG 2.5.5 (AAA) recommends 44x44px, and iOS defaults to that threshold for hit testing anyway.

Shipping It: Accessibility, Dark Backgrounds, and Empire UI Tokens

Before you call this done, run through a quick checklist. Focus management: on each step transition, focus needs to move to the new step's first interactive element. Use a ref on the step heading and call .focus() after the animation settles — a 300ms setTimeout after setStep is the pragmatic answer, though an onAnimationComplete callback from Framer Motion is cleaner.

Keyboard navigation should work without a mouse. Tab through all fields, hit Enter on the Next button, and Shift+Tab back. If your step has a single primary field (like a name input), auto-focus it on mount with autoFocus — just don't do this on mobile where it pops the keyboard unexpectedly before the user is ready.

For dark-mode support, swap your gradient to from-gray-900 via-slate-800 to-gray-900 and drop the glass opacity slightly: bg-white/8 border-white/12. The frosted effect still reads — you just need a little more contrast boost on your text: text-white everywhere and text-white/70 for helper text. Check out the dark glassmorphism navbar article for the exact dark-mode token values that work.

The glassmorphism components on Empire UI include modal, card, input, and button variants that are already calibrated for both light and dark gradient backgrounds. Dropping those in instead of hand-rolling every surface will save you a few hours of tweaking and get you to a consistent visual baseline faster. Combine them with the gradient generator to nail your background palette before you wire up any state logic.

That's the full stack: glass panel with separated blur surface, animated step transitions, accessible progress indicator, validated per-step state, and glass navigation buttons. It's not a lot of code. The wizard pattern often feels intimidating before you write it, but once it's broken into these five layers it's just a few composable components that happen to look great together.

FAQ

Can I use glassmorphism in a multi-step wizard without Framer Motion?

Yes — plain CSS transitions on opacity and transform handle step entry/exit fine. Just add transition: opacity 200ms ease, transform 200ms ease and toggle classes. Framer Motion's AnimatePresence is a convenience, not a requirement.

How do I persist form data across wizard steps without a library?

Keep a single formData object in useState at the wizard root and merge each step's data into it via a callback: setFormData(prev => ({ ...prev, ...stepData })). That's genuinely all you need for a 3-5 step onboarding flow.

Does backdrop-filter: blur work on mobile for wizard UIs?

It does, but backdrop-blur-xl (24px) can cause frame drops on mid-range Android devices. Use backdrop-blur-md (12px) for step panels on mobile — the frosted look holds and the GPU compositing cost drops significantly.

What's the right number of steps for an onboarding wizard?

Three to five. Below three you probably don't need a wizard at all. Above five, abandonment rates climb sharply on step two regardless of visual polish. If you have more required fields, split them across sub-sections within a single step rather than adding more steps.

Free components in 40 styles
React & Tailwind, copy-paste ready.
Browse →

Read next

Glassmorphism Onboarding Steps: Frosted Progress and Feature SlidesSaaS Onboarding UI: Checklists, Progress Steps and Empty StatesOnboarding Flow in React: Multi-Step, Spotlight and Tooltip ToursStepper Component in React: Multi-Step Forms and Onboarding