All Taste components
Animated screens
Onboarding carousel
Three first-run slides that advance on their own, with a parallax illustration and step dots that stretch into a pill.
3 slides · auto-advance · parallax · morphing dots
Wayfarer
Already have an account? Sign in
// Onboarding carousel: slides auto-advance (and swipe), the backdrop moves slower than the art (parallax),
// the step dots stretch into a pill. Mobile-first page on the Taste preset; maps to React Native via NativeWind.
"use client";
import { useEffect, useRef, useState } from "react";
import { MapPin } from "lucide-react";
import { cn } from "@/lib/utils";
const slides = [
{ title: "Plan trips together", body: "Share one itinerary with friends and vote on stays, stops and dinner spots.", art: <MapArt /> },
{ title: "Book in one tap", body: "Flights, trains and stays in one checkout, with tickets saved to your trip.", art: <TicketArt /> },
{ title: "Your trip, even offline", body: "Maps, tickets and bookings stay on your phone when the signal drops.", art: <OfflineArt /> },
];
export default function Onboarding({ onDone }: { onDone?: () => void }) {
const [step, setStep] = useState(0);
const [paused, setPaused] = useState(false);
const startX = useRef<number | null>(null);
const last = step === slides.length - 1;
// Auto-advance every 3.5 s until the last slide, or until the user takes over.
useEffect(() => {
if (paused || last) return;
const t = setTimeout(() => setStep((s) => s + 1), 3500);
return () => clearTimeout(t);
}, [step, paused, last]);
const go = (n: number) => {
setPaused(true);
setStep(Math.max(0, Math.min(slides.length - 1, n)));
};
return (
<main className="mx-auto flex min-h-dvh max-w-md flex-col bg-bg px-4 pb-5 font-sans text-fg">
<header className="flex h-14 items-center justify-between">
<span className="flex items-center gap-2 text-[15px] font-semibold">
<span className="grid size-[26px] place-items-center rounded-sm bg-primary text-primary-fg"><MapPin className="size-[15px]" /></span>
Wayfarer
</span>
<button type="button" onClick={onDone} className="-mr-2 h-10 rounded px-2 text-sm font-medium text-fg-muted hover:bg-surface-2">Skip</button>
</header>
<div
className="relative mt-1 min-h-56 flex-1 touch-pan-y overflow-hidden rounded-xl bg-primary-soft"
onPointerDown={(e) => (startX.current = e.clientX)}
onPointerUp={(e) => {
if (startX.current === null) return;
const dx = e.clientX - startX.current;
startX.current = null;
if (Math.abs(dx) > 40) go(step + (dx < 0 ? 1 : -1));
}}
>
{/* Backdrop: a repeating pattern that moves a third as far as the slides */}
<div
aria-hidden
className="absolute inset-0 transition-[background-position] duration-700 ease-taste motion-reduce:transition-none"
style={{
backgroundImage:
"radial-gradient(circle at 40px 56px, color-mix(in srgb, var(--tst-primary) 16%, transparent) 0 26px, transparent 27px), radial-gradient(circle, color-mix(in srgb, var(--tst-primary) 24%, transparent) 1.2px, transparent 1.8px)",
backgroundSize: "180px 100%, 20px 20px",
backgroundPosition: -step * 60 + "px 0, " + -step * 60 + "px 0",
}}
/>
<div className="relative flex h-full transition-transform duration-700 ease-taste motion-reduce:transition-none" style={{ transform: "translateX(" + -step * 100 + "%)" }}>
{slides.map((s, i) => (
<div key={s.title} aria-hidden={i !== step} className="grid w-full shrink-0 place-items-center p-2">{s.art}</div>
))}
</div>
</div>
<div className="mt-5 overflow-hidden">
<div className="flex transition-transform duration-700 ease-taste motion-reduce:transition-none" style={{ transform: "translateX(" + -step * 100 + "%)" }}>
{slides.map((s, i) => (
<section key={s.title} aria-hidden={i !== step} className="w-full shrink-0">
<h1 className="text-[22px] font-semibold leading-7 tracking-tight">{s.title}</h1>
<p className="mt-2 text-sm text-fg-muted">{s.body}</p>
</section>
))}
</div>
</div>
<div className="my-4 flex gap-1.5">
{slides.map((s, i) => (
<button
key={s.title}
type="button"
aria-label={"Go to step " + (i + 1)}
aria-current={i === step ? "step" : undefined}
onClick={() => go(i)}
className={cn("h-1.5 rounded-full transition-all duration-500 ease-taste motion-reduce:transition-none", i === step ? "w-[22px] bg-primary" : "w-1.5 bg-border-strong")}
/>
))}
</div>
<button type="button" onClick={() => (last ? onDone?.() : go(step + 1))} className="h-12 w-full rounded bg-primary text-[15px] font-semibold text-primary-fg hover:bg-primary-hover">
<span key={String(last)} className="inline-block animate-rise motion-reduce:animate-none">{last ? "Get started" : "Next"}</span>
</button>
<p className="mt-3 text-center text-[13px] text-fg-muted">Already have an account? <a href="/sign-in" className="font-medium text-primary">Sign in</a></p>
</main>
);
}
function MapArt() {
return (
<svg viewBox="0 0 252 236" className="h-full max-h-60 w-full" aria-hidden>
<rect x="40" y="34" width="172" height="150" rx="16" className="fill-bg" />
<path d="M40 84h172M40 134h172M97 34v150M155 34v150" className="stroke-border" strokeWidth="1.5" />
<path d="M70 156c26-50 70-6 96-46 10-16 12-28 16-44" fill="none" className="stroke-primary" strokeWidth="3.5" strokeLinecap="round" strokeDasharray="2 8" />
<circle cx="70" cy="156" r="7" className="fill-bg stroke-primary" strokeWidth="3.5" />
<path d="M182 72c-9-10-14-17-14-24a14 14 0 0 1 28 0c0 7-5 14-14 24z" className="fill-primary" />
<circle cx="182" cy="48" r="5" className="fill-bg" />
<rect x="130" y="176" width="92" height="36" rx="18" className="fill-bg" />
<circle cx="150" cy="194" r="11" className="fill-[var(--tst-warning-300)] stroke-bg" strokeWidth="3" />
<circle cx="166" cy="194" r="11" className="fill-[var(--tst-success-300)] stroke-bg" strokeWidth="3" />
<circle cx="182" cy="194" r="11" className="fill-brand-300 stroke-bg" strokeWidth="3" />
<text x="198" y="198" className="fill-fg text-[11px] font-semibold">+2</text>
</svg>
);
}
function TicketArt() {
const bars = [0, 6, 10, 16, 19, 25, 31, 34, 40, 46, 49, 55, 61, 64, 70, 76, 79, 85, 91, 96, 100, 106, 112];
return (
<svg viewBox="0 0 252 236" className="h-full max-h-60 w-full" aria-hidden>
<path d="M50 52a14 14 0 0 1 14-14h124a14 14 0 0 1 14 14v68a11 11 0 0 0 0 22v38a14 14 0 0 1-14 14H64a14 14 0 0 1-14-14v-38a11 11 0 0 0 0-22z" className="fill-bg" />
<path d="M64 131h124" className="stroke-border-strong" strokeWidth="1.5" strokeDasharray="4 5" />
<text x="68" y="84" className="fill-fg text-2xl font-bold">BLR</text>
<text x="68" y="102" className="fill-fg-muted text-[10px] font-medium">Bengaluru</text>
<text x="184" y="84" textAnchor="end" className="fill-fg text-2xl font-bold">GOI</text>
<text x="184" y="102" textAnchor="end" className="fill-fg-muted text-[10px] font-medium">Goa</text>
<path d="M118 78h16m-6-6 6 6-6 6" fill="none" className="stroke-primary" strokeWidth="2.5" strokeLinecap="round" strokeLinejoin="round" />
{bars.map((x, i) => <rect key={x} x={70 + x} y="148" width={i % 3 === 0 ? 3 : 2} height="22" className="fill-fg" />)}
<circle cx="200" cy="42" r="18" className="fill-success stroke-bg" strokeWidth="4" />
<path d="m192 42 6 6 10-11" fill="none" className="stroke-bg" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function OfflineArt() {
return (
<svg viewBox="0 0 252 236" className="h-full max-h-60 w-full" aria-hidden>
<rect x="84" y="24" width="88" height="180" rx="16" className="fill-bg stroke-fg" strokeWidth="4" />
<rect x="114" y="32" width="28" height="6" rx="3" className="fill-fg" />
<path d="M92 70c22 6 30 26 52 22s22 22 24 30M92 140c18-14 40-4 52 10s20 10 24 6" fill="none" className="stroke-border-strong" strokeWidth="2" />
<path d="M104 160c10-24 30-30 30-52 0-12 10-20 20-30" fill="none" className="stroke-primary" strokeWidth="3.5" strokeLinecap="round" />
<circle cx="104" cy="160" r="6" className="fill-primary" />
<circle cx="154" cy="78" r="6" className="fill-bg stroke-primary" strokeWidth="3.5" />
<path d="M42 98a16 16 0 0 1 31-6 12 12 0 1 1 3 24H45a13 13 0 0 1-3-18z" className="fill-bg" />
<path d="m48 88 26 30" className="stroke-fg-muted" strokeWidth="2.5" strokeLinecap="round" />
<circle cx="180" cy="176" r="20" className="fill-primary stroke-bg" strokeWidth="4" />
<path d="M180 166v18m-7-7 7 7 7-7" fill="none" className="stroke-primary-fg" strokeWidth="2.6" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.