Skip to content
All Taste components

Onboarding

Shopping onboarding

A shopping app's intro with a bag and price tags among floating shapes, and a round next button.

floating shapes · centred text · dots + round arrow button · skip

Deals picked just for you

Save what you love and we will tell you when the price drops.

// Mobile-first screen for a React web app; the same markup maps to React Native via NativeWind (View, Text, Pressable, react-native-svg).
import { cn } from "@/lib/utils";

const slides = [
  { title: "New drops, every morning", body: "Fresh picks from brands you follow, first thing each day." },
  { title: "Deals picked just for you", body: "Save what you love and we will tell you when the price drops." },
  { title: "Easy returns", body: "Changed your mind? Book a free pickup within 15 days." },
];

function Illustration({ className }: { className?: string }) {
  return (
    <svg viewBox="0 0 240 210" aria-hidden="true" className={cn("h-full w-full", className)}>
      <circle cx="44" cy="58" r="12" className="fill-brand-200" />
      <circle cx="202" cy="42" r="10" strokeWidth="4" fill="none" className="stroke-brand-400" />
      <path d="M200 148l14 24h-28z" strokeWidth="4" strokeLinejoin="round" className="fill-brand-200 stroke-brand-200" />
      <rect x="26" y="150" width="16" height="16" rx="3" transform="rotate(20 34 158)" className="fill-brand-400" />
      <circle cx="216" cy="102" r="5" className="fill-brand-600" />
      <path d="M94 74V62a26 26 0 0 1 52 0v12" strokeWidth="7" strokeLinecap="round" strokeLinejoin="round" fill="none" className="stroke-brand-800" />
      <path d="M66 74h108l10 112a8 8 0 0 1-8 9H64a8 8 0 0 1-8-9z" className="fill-brand-400" />
      <rect x="63" y="74" width="114" height="16" className="fill-brand-600" />
      <circle cx="94" cy="104" r="4" className="fill-brand-800" />
      <circle cx="146" cy="104" r="4" className="fill-brand-800" />
      <path d="M126 120h42l14 16-14 16h-42a6 6 0 0 1-6-6v-20a6 6 0 0 1 6-6z" transform="rotate(-16 150 140)" className="fill-[var(--tst-warning-300)]" />
      <circle cx="167" cy="136" r="4" transform="rotate(-16 150 140)" className="fill-brand-400" />
      <path d="M134 144l16-16" strokeWidth="3" transform="rotate(-16 150 140)" strokeLinecap="round" strokeLinejoin="round" fill="none" className="stroke-[var(--tst-warning-500)]" />
      <circle cx="135" cy="130" r="3" transform="rotate(-16 150 140)" className="fill-[var(--tst-warning-500)]" />
      <circle cx="149" cy="142" r="3" transform="rotate(-16 150 140)" className="fill-[var(--tst-warning-500)]" />
      <path d="M74 150h30l10 11-10 11H74a5 5 0 0 1-5-5v-12a5 5 0 0 1 5-5z" transform="rotate(14 92 160)" className="fill-neutral-50" />
      <circle cx="103" cy="161" r="3" transform="rotate(14 92 160)" className="fill-brand-400" />
      <path d="M77 161h16" strokeWidth="3" transform="rotate(14 92 160)" strokeLinecap="round" strokeLinejoin="round" fill="none" className="stroke-brand-200" />
    </svg>
  );
}

function Dots({ step, total, className }: { step: number; total: number; className?: string }) {
  return (
    <div className={cn("flex gap-1.5", className)} role="img" aria-label={"Step " + (step + 1) + " of " + total}>
      {Array.from({ length: total }, (_, i) => (
        <span key={i} className={cn("h-1.5 rounded-full transition-all", i === step ? "w-[22px] bg-primary" : "w-1.5 bg-border-strong")} />
      ))}
    </div>
  );
}

export default function ShoppingOnboarding({ step = 1, onNext, onSkip }: { step?: number; onNext?: () => void; onSkip?: () => void }) {
  const slide = slides[step];
  return (
    <main className="mx-auto flex min-h-dvh max-w-md flex-col bg-bg px-6 pb-8 font-sans text-fg">
      <header className="-mr-3.5 flex h-12 items-center justify-end">
        <button type="button" onClick={onSkip} className="h-11 rounded px-3.5 text-sm font-medium text-fg-muted hover:bg-surface-2">
          Skip
        </button>
      </header>
      <div className="min-h-0 flex-1 py-2">
        <Illustration />
      </div>
      <div className="mb-8 mt-5">
        <h1 className="text-center text-[26px] font-semibold leading-8 tracking-tight [text-wrap:balance]">{slide.title}</h1>
        <p className="mt-2.5 text-center text-[15px] leading-[22px] text-fg-muted">{slide.body}</p>
      </div>
      <div className="flex items-center justify-between">
        <Dots step={step} total={slides.length} />
        <button type="button" onClick={onNext} aria-label="Next" className="grid size-[60px] place-items-center rounded-full bg-primary text-primary-fg hover:bg-primary-hover">
          <svg viewBox="0 0 24 24" aria-hidden="true" className="size-[22px]" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
            <path d="M5 12h14M13 6l6 6-6 6" />
          </svg>
        </button>
      </div>
    </main>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.