Onboarding Carousel React component for Tailwind CSS v4
v1.0.0Added in 1.0.0
A React onboarding carousel of first-run slides for an app or PWA: swipe or tap Next, follow the dots, Skip any time, then Get started.
Swipe the slides with a finger, or drag them with a mouse or trackpad (embla's physics, vertical page scrolling is never blocked); the illustration trails the swipe at 0.6x and the text at 0.9x, and each slide's background color cross-fades into the next as you go; a finger swipe that lands on a new slide ticks once (haptics). Tap Next to advance; on the last slide it becomes Get started with a quick fade (the circle variant grows from a round arrow, ringed by the progress, into a pill). Skip in the top corner jumps to the last slide (or finishes right away with skipTo="complete") and fades out on the last slide; with showBack a back chevron slides in before Next from the second slide. Tap a dot or a story bar to jump there. With focus anywhere inside, Left and Right move one slide and Home / End jump to the ends; Enter or Space on Next advances, and focus is handed to Next when Skip or Back disappear. Each settled slide is announced ("Slide 2 of 4: ..."), off-screen slides are inert. Finishing fades the whole thing out before onComplete runs; with persistKey the finish (or a skip that ends it: skipTo="complete", closing the modal) is remembered and it never shows again. As a modal it opens from its trigger as a centered card on desktop and slides up as a full screen on a phone, focus lands on Next, and Escape or a click outside counts as Skip. Under reduced motion slides jump, there is no parallax, and everything else fades.
Onboarding Carousel is a Pro React component in the React mobile components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add onboarding-carousel, together with Button, Carousel, Haptics, useControllableState, useMediaQuery and 4 more and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, motion and lucide-react.
Add to a project
Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup
npx wingo-ui@latest add onboarding-carouselOnboarding Carousel is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans
Usage
Import OnboardingCarousel from @/components/ui/onboarding-carousel.
"use client"
import { useRouter } from "next/navigation"
import { OnboardingCarousel, type OnboardingSlide } from "@/components/ui/onboarding-carousel"
const slides: OnboardingSlide[] = [
{
id: "clients",
title: "Every client in one place",
description: "Companies, contacts and balances, the same on your phone and at the office.",
image: "/onboarding/clients.svg",
background: "var(--info-soft)",
},
{
id: "invoices",
title: "Invoices in 30 seconds",
description: "Issue the invoice from your phone and we email it to your client.",
image: "/onboarding/invoices.svg",
background: "var(--success-soft)",
},
{
id: "offline",
title: "Works offline too",
description: "Everything syncs when you are back online.",
image: "/onboarding/offline.svg",
},
]
export default function Welcome() {
const router = useRouter()
return (
<OnboardingCarousel
layout="screen"
slides={slides}
persistKey="onboarding-v1"
showBack
labels={{ skip: "Skip", next: "Continue", back: "Back", done: "Get started", region: "Welcome" }}
onComplete={() => router.replace("/home")}
/>
)
}Props
slides*OnboardingSlide[]{ id, title, description?, media?, image?, imageAlt?, background?, foreground? }. media is any node (a composed ui vignette, an svg); image is contained in the media area, or full-bleed behind the text with a bottom scrim when foreground is "light". background is any CSS color or token (var(--info-soft)) and cross-fades while swiping; without one the root's surface shows.
layoutdefault"fill""fill" | "screen" | "card"fill takes the parent's height (a route of its own inside an app shell). screen is min-h-dvh with safe-area padding at the top and bottom. card is a 420 x 640px rounded surface, capped at 85dvh.
modaldefaultfalsebooleanRenders it in a Radix dialog with a focus trap and a hidden title: a full screen below 768px that slides up, a centered card from 768px. Closing (Escape, the overlay) counts as Skip. layout is ignored.
defaultOpendefaultfalsebooleanModal mode: open on mount. The playground turns modal on with it.
openbooleanModal mode, controlled. Pair with onOpenChange.
onOpenChange(open: boolean) => voidModal mode: the trigger opened it, or Skip, Escape, the overlay or the last button closed it.
triggerReactNodeModal mode: the element that opens it, rendered asChild (a Button).
indicatordefault"dots""dots" | "bars" | "progress" | "counter"The carousel's indicators: dots whose active one stretches, story bars, one progress bar, or a 2 / 4 counter. Hidden with a single slide.
indicatorPositiondefault"bottom""top" | "bottom"bottom sits above Next (beside the circle). top puts it in the header next to Skip, the stories look.
nextVariantdefault"button""button" | "circle"button: a full-width Next that becomes Get started. circle: a round arrow ringed by the progress that grows into the Get started pill on the last slide.
sizedefault"md""sm" | "md" | "lg"Title 24 / 28 / 32px, description 15 / 16 / 17px, indicators, and the Next button (40 / 48 / 48px, 44px on touch at sm; the circle 48 / 56 / 64px).
aligndefault"center""center" | "start"Text alignment of the title and description.
tonedefault"neutral""neutral" | "brand" | "success" | "warning" | "danger" | "info"The Next / Get started button, the circle's ring and the active dot or bar.
colordefault""stringAny CSS color. Replaces the tone; text on the button turns black or white by lightness.
radiusdefault"md""none" | "sm" | "md" | "lg" | "full"Corner shape of the Next and Back buttons in the button variant (the circle is always round).
showSkipdefaulttruebooleanThe Skip button in the header. It fades out on the last slide.
showBackdefaultfalsebooleanA back chevron before Next from the second slide.
skipTodefault"last""last" | "complete"Skip jumps to the last slide, or finishes right away (fade out, then onSkip and onComplete).
parallaxdefaulttruebooleanWhile swiping, the media moves at 0.6x the slide and the text at 0.9x. Off under reduced motion.
hapticsdefaulttruebooleanA "selection" tick through lib/haptics when a finger or pen swipe lands on a new slide. Never on a mouse drag, taps, keys or mount; silent on desktops.
persistKeydefault""stringWhen set, a finish or a skip that ends the tour (skipTo="complete", closing the modal) is remembered in localStorage (use-persisted-state) and nothing renders afterwards; a modal stays closed unless its trigger opens it. The server renders the slides; a remembered finish fades them out after hydration (a modal never flashes). Clear it with writePersisted(key, null).
indexnumberThe current slide, controlled. A jump scrolls there (instantly under reduced motion); values past the end clamp.
defaultIndexdefault0numberThe first slide shown, uncontrolled. The tour also returns here after it finishes.
onIndexChange(index: number) => voidEvery settled change: a swipe, Next, Back, Skip, a dot or a key.
onComplete() => voidThe last slide's button (and Skip with skipTo="complete"), after the fade out.
onSkip(index: number) => voidSkip was pressed (or the modal was closed) on this slide.
labelsPartial<OnboardingCarouselLabels>skip "Skip", next "Next", back "Back", done "Get started", region "Welcome" (the region label and the modal title fallback), slide(i, n) "Slide 2 of 4", goTo(i) "Go to slide 3". Pass only the keys you translate.
setApi(api: CarouselApi) => voidThe embla api, for custom controls.
classNamesPartial<Record<"header" | "skip" | "slide" | "media" | "title" | "description" | "footer" | "indicator" | "back" | "next", string>>Extra classes for the inner parts.
classNamestringExtra classes for the root, merged last. Override the tone vars here, e.g. [--tone:#0f766e].
Parts
OnboardingSlide
typeOne slide: id, title, description, media (any node) or image with imageAlt, background (any CSS color, cross-fades while swiping) and foreground ("light" for photos and dark backgrounds).
DEFAULT_ONBOARDING_CAROUSEL_LABELS
constantThe English copy: skip "Skip", next "Next", back "Back", done "Get started", region "Welcome" and the slide announcements.
Examples
Circle next, brand tone
A round arrow ringed by the progress that grows into the Get started pill on the last slide.
As a modal
A trigger opens it as a centered card on desktop and a full screen on a phone.
Photo slides
Full-bleed photos with foreground="light": white text and controls over a scrim.
Stories bars on top
indicatorPosition="top" puts the bars in the header next to Skip.
Remembered once seen
persistKey stores a finish or a skip; Reset clears it and the tour comes back.
Start aligned, large, default labels
align="start", size="lg", the progress bar and the built-in default labels.
Dependencies
Files
- components/ui/onboarding-carousel.tsx
FAQ
Is the React Onboarding Carousel component free?
The preview, props and docs are free; copying the source needs a Pro plan. See plans.
How do I install Onboarding Carousel?
Run npx wingo-ui@latest add onboarding-carousel. It also adds the Wingo UI items it builds on (Button, Carousel, Haptics, useControllableState, useMediaQuery and 4 more) and the npm packages @radix-ui/react-dialog, @radix-ui/react-visually-hidden, motion and lucide-react. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/onboarding-carousel.
How does Onboarding Carousel behave on phones?
As a modal it opens from its trigger as a centered card on desktop and slides up as a full screen on a phone, focus lands on Next, and Escape or a click outside counts as Skip.
See also
Used in
More React mobile components
Articles using this component
- Embla Carousel React: Accessible Slides, No Layout Shift
Component guides9 min read
- React Onboarding Flow: Slides, Permissions, Install
Build it: screens and apps9 min read