Glassmorphism Onboarding Screens: Welcome Flow with Blur
Build glassmorphism onboarding screens with frosted blur panels, step indicators, and smooth transitions. Real code, Tailwind v4, no fluff.
Why Glassmorphism Works for Onboarding
Honestly, first impressions matter more than any engineer wants to admit. Your onboarding flow is the first real interaction a user has with your product, and a generic white card on a white background tells them nothing about your brand.
Glassmorphism — frosted blur panels layered over vivid gradient backgrounds — creates depth immediately. The backdrop-filter: blur() effect makes your UI feel like it exists in physical space. Users notice. That noticeability buys you the 30 seconds you need to explain what your app actually does.
There's also a practical argument. Onboarding screens are modal by nature: they sit above your app, overlay content, and demand attention. That's exactly the visual metaphor that glass panels communicate without any extra effort. You're not fighting the UI pattern, you're working with it.
If you're starting from scratch with the concept, what is glassmorphism is a solid primer before you touch any code.
The Core CSS Stack: Backdrop Filter and Transparency
Everything in a glassmorphism onboarding component rests on three CSS properties. Get these right and the rest is styling.
// GlassPanel.tsx
import { ReactNode } from 'react';
interface GlassPanelProps {
children: ReactNode;
className?: string;
}
export function GlassPanel({ children, className = '' }: GlassPanelProps) {
return (
<div
className={`
relative
rounded-2xl
border border-white/20
bg-white/10
backdrop-blur-md
shadow-[0_8px_32px_rgba(0,0,0,0.25)]
${className}
`}
>
{children}
</div>
);
}The bg-white/10 gives you rgba(255,255,255,0.10) — just enough translucency to see the background gradient through the panel. backdrop-blur-md maps to backdrop-filter: blur(12px) in Tailwind v4.0.2, which is the sweet spot. Go higher than blur-xl (24px) and you lose the layering effect entirely; the background just smears into grey.
The border is important. border-white/20 at rgba(255,255,255,0.20) catches light and separates the panel from the background without a harsh edge. Skip it and the whole thing looks flat.
Building the Multi-Step Welcome Flow
Most onboarding flows have 3–5 steps: welcome, feature highlights, account setup, maybe a plan selection. You want a single animated container that transitions content, not a full page reload between steps.
// OnboardingFlow.tsx
import { useState } from 'react';
import { GlassPanel } from './GlassPanel';
import { AnimatePresence, motion } from 'framer-motion';
const STEPS = [
{ id: 0, title: 'Welcome', subtitle: "Let's get you set up in 2 minutes." },
{ id: 1, title: 'Pick a style', subtitle: 'Choose how your dashboard looks.' },
{ id: 2, title: 'Invite your team', subtitle: 'Skip this — you can do it later.' },
];
export function OnboardingFlow() {
const [step, setStep] = useState(0);
const current = STEPS[step];
return (
<div className="min-h-screen flex items-center justify-center bg-gradient-to-br from-violet-700 via-purple-600 to-pink-500 p-6">
<GlassPanel className="w-full max-w-md p-8">
{/* Step indicator */}
<div className="flex gap-2 mb-8">
{STEPS.map((s) => (
<div
key={s.id}
className={`h-1 flex-1 rounded-full transition-all duration-300 ${
s.id <= step ? 'bg-white' : 'bg-white/25'
}`}
/>
))}
</div>
{/* Animated content */}
<AnimatePresence mode="wait">
<motion.div
key={step}
initial={{ opacity: 0, y: 12 }}
animate={{ opacity: 1, y: 0 }}
exit={{ opacity: 0, y: -12 }}
transition={{ duration: 0.22 }}
>
<h1 className="text-2xl font-bold text-white mb-2">{current.title}</h1>
<p className="text-white/70 mb-8 text-sm leading-relaxed">{current.subtitle}</p>
</motion.div>
</AnimatePresence>
{/* Navigation */}
<div className="flex justify-between">
<button
onClick={() => setStep((s) => Math.max(0, s - 1))}
disabled={step === 0}
className="text-white/50 text-sm disabled:opacity-30 hover:text-white transition-colors"
>
Back
</button>
<button
onClick={() => setStep((s) => Math.min(STEPS.length - 1, s + 1))}
className="px-5 py-2 rounded-xl bg-white/20 hover:bg-white/30 text-white text-sm font-medium transition-colors border border-white/25"
>
{step === STEPS.length - 1 ? 'Get started' : 'Continue'}
</button>
</div>
</GlassPanel>
</div>
);
}The step indicator strip is just a flex row of 1px-height divs that transition opacity. Simple, no external library needed. The AnimatePresence from Framer Motion handles the crossfade — mode="wait" ensures the exit animation completes before the next step enters, which prevents content overlap during the transition.
Background Gradient Setup That Actually Looks Good
Your background gradient is doing half the work. A glass panel over a flat colour looks broken. You need chroma and depth behind it.
For onboarding flows, three-stop gradients tend to photograph well and render consistently across displays. Something like from-violet-700 via-purple-600 to-pink-500 gives you enough colour variance that the blur has something to pull from. You can also add a decorative element — a large blurred circle or two — to introduce variance in the background itself.
/* If you're not using Tailwind's gradient utilities */
.onboarding-bg {
background: linear-gradient(
135deg,
#5b21b6 0%,
#7c3aed 40%,
#db2777 100%
);
position: relative;
overflow: hidden;
}
.onboarding-bg::before {
content: '';
position: absolute;
width: 480px;
height: 480px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.08);
filter: blur(80px);
top: -120px;
left: -80px;
}
.onboarding-bg::after {
content: '';
position: absolute;
width: 360px;
height: 360px;
border-radius: 50%;
background: rgba(255, 255, 255, 0.06);
filter: blur(60px);
bottom: -100px;
right: -60px;
}Those pseudo-element blobs are what give the glass panel something interesting to refract. Without them you'll get a uniform blur that looks like a CSS mistake rather than a design choice. For a more interactive take on animated backgrounds, particles background react covers particle-based backgrounds that pair well with glass panels.
Handling Dark Mode Without Breaking the Blur Effect
Dark mode and glassmorphism have a complicated relationship. In light mode, white-tinted glass panels over colourful gradients is the default recipe. In dark mode, if you just flip the background to a dark gradient, the bg-white/10 panel becomes nearly invisible.
The fix: swap your panel base colour in dark mode. Use dark:bg-white/5 paired with a lighter border dark:border-white/10, and shift your background gradient to deeper, more saturated dark tones like dark:from-slate-900 dark:via-purple-950 dark:to-slate-900.
You'll also want to check your shadow. shadow-[0_8px_32px_rgba(0,0,0,0.25)] works in light mode but in dark mode you might want dark:shadow-[0_8px_32px_rgba(0,0,0,0.6)] for the panel to register visually against a dark background. Check theme toggle react if you need a working dark mode toggle implementation to test with.
One thing people miss: backdrop-blur interacts with the stacking context. If your glass panel is inside a transform, will-change, or filter parent, the blur clips to that parent's bounds. This bites you most often when you add slide-in animations to the entire background — wrap only the panel, not the background, in your motion components.
Accessibility: Glass Doesn't Have to Mean Unreadable
Here's the honest problem with glassmorphism: WCAG contrast ratios and semi-transparent panels don't get along. rgba(255,255,255,0.10) over a gradient gives you unpredictable contrast depending on which part of the gradient sits behind your text at any given viewport width.
The solution isn't to abandon the aesthetic — it's to be deliberate. Put your text on a slightly darker sub-panel inside the glass card. Something like bg-black/20 rounded-xl px-4 py-3 for your copy blocks gives you a consistent dark backing without destroying the glass effect on the outer panel. Then you can confidently use text-white knowing the contrast is there.
Also: never rely on blur alone to separate interactive elements. Add focus rings. The default Tailwind focus-visible:ring-2 focus-visible:ring-white works fine on glass backgrounds. Users navigating with keyboards can't see your beautiful backdrop-filter, and they shouldn't have to.
Worth reading the component examples in best free glassmorphism components — the ones that hold up in production all have some form of contrast fallback built in.
Performance: backdrop-filter Is Expensive
Real talk — backdrop-filter: blur() triggers GPU compositing on every repaint of the blurred area. On a landing page with a static background this is fine. On a multi-step onboarding flow with CSS animations running simultaneously, you need to be deliberate.
Two rules: First, limit the number of elements using backdrop-filter to one or two per screen. Stack multiple glass layers and you'll see jank on mid-range Android devices. Second, avoid animating the blur value itself (e.g. blurring from 0 to 12px as an enter animation). The browser has to repaint the composite layer on every frame. Animate opacity and transform instead — those are cheap.
If you're building for a React SPA, adding will-change: transform to your glass panel's parent wrapper tells the browser to hoist it to its own compositor layer ahead of time. That prevents layout thrash during step transitions. Remove the will-change once the animation completes — leaving it on permanently wastes memory. And if you're curious how glassmorphism stacks up against neumorphism from a performance standpoint, glassmorphism vs neumorphism gets into the tradeoffs.
Integrating with Empire UI Glass Components
Empire UI ships a GlassCard component under the Glass style that's pre-wired with the right opacity, blur, and border defaults. You don't have to hand-roll everything above from scratch if you're already using the library.
The onboarding-specific thing you'll add on top is the step state and transition logic. Empire UI doesn't prescribe a flow — it gives you composable panels. Combine GlassCard with your own useState step counter, drop in Framer Motion for transitions, and you have a complete onboarding experience in under 150 lines.
What makes Empire UI's glass components worth using over rolling your own isn't just the defaults — it's that they're tested across 40 visual style variants. If your product lets users pick their UI style (like a theme switcher), the same onboarding component works under both Glass and Neumorphism styles without a rewrite. That kind of flexibility matters when you're iterating fast on a startup product.
FAQ
Safari, Chrome, and Edge all support backdrop-filter fully. Firefox requires no prefix and has supported it since Firefox 103. The only real gap is older Android WebView versions below Chromium 76. For a fallback, add a solid bg-white/30 or bg-black/40 fallback before the backdrop-filter rule — it degrades to a translucent panel without blur, which is still readable.
Glass panels need colour behind them. If your gradient is too light or desaturated, increase saturation first — try HSL values with saturation above 60%. Alternatively increase the border opacity to border-white/35 and add a slight inner shadow with shadow-inner. The illusion needs contrast between the panel and the background to work.
In Tailwind v4.0.2, backdrop-blur-md (12px) is the practical ceiling on mobile. backdrop-blur-lg (16px) starts to stutter on older iPhones during animated transitions. If you're building mobile-first, test on a real device, not just Chrome DevTools. The simulator doesn't reflect GPU compositing cost accurately.
Use Framer Motion's AnimatePresence with mode="wait". This makes the exiting component fully unmount before the entering component mounts, preventing both from being in the DOM simultaneously. A duration of 0.2–0.25s feels snappy without being abrupt. Avoid mode="sync" for step flows — you'll get layout shift.
Yes, with one caveat. backdrop-filter is a browser-only CSS property — it simply doesn't render on the server. Since Next.js SSR sends HTML without a browser compositor, the initial HTML will show your fallback background colour until hydration. This is fine; users see the glass effect the moment the component hydrates, which is near-instant. No special handling required.
Progress bars (thin horizontal strips) read more naturally in glass UIs because they avoid the contrast issues that small dots have on blurred backgrounds. A 4px height strip with rounded-full reads clearly at any opacity. Dots below roughly 10px diameter can disappear into the blur if the background gradient passes through a similar hue behind them.