All Taste components
Onboarding
Savings onboarding
A budgeting app's first screen: a piggy bank illustration above a short promise and one clear next step.
illustration top · left-aligned text · 3-step dots · skip · next
Save a little, every day
Round up each payment and watch your savings grow on their own.
// 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: "Save a little, every day", body: "Round up each payment and watch your savings grow on their own." },
{ title: "See where it all goes", body: "Spending sorts itself into simple categories you can check at a glance." },
{ title: "Set a goal, hit it", body: "Pick a target and a date. We work out the weekly amount for you." },
];
function Illustration({ className }: { className?: string }) {
return (
<svg viewBox="0 0 240 200" aria-hidden="true" className={cn("h-full w-full", className)}>
<ellipse cx="120" cy="184" rx="86" ry="7" opacity="0.12" className="fill-primary" />
<rect x="186" y="164" width="40" height="13" rx="6.5" strokeWidth="2.5" className="fill-[var(--tst-warning-300)] stroke-[var(--tst-warning-500)]" />
<rect x="190" y="151" width="40" height="13" rx="6.5" strokeWidth="2.5" className="fill-[var(--tst-warning-300)] stroke-[var(--tst-warning-500)]" />
<rect x="184" y="138" width="40" height="13" rx="6.5" strokeWidth="2.5" className="fill-[var(--tst-warning-300)] stroke-[var(--tst-warning-500)]" />
<path d="M62 118c-13 0-15-13-5-14 7 0 7 9 0 10" strokeWidth="4" strokeLinecap="round" strokeLinejoin="round" fill="none" className="stroke-brand-600" />
<rect x="82" y="148" width="17" height="30" rx="7" className="fill-brand-600" />
<rect x="134" y="148" width="17" height="30" rx="7" className="fill-brand-600" />
<path d="M90 90 97 62l22 20z" strokeWidth="6" strokeLinejoin="round" className="fill-brand-600 stroke-brand-600" />
<ellipse cx="118" cy="122" rx="58" ry="45" className="fill-brand-400" />
<path d="M80 110a40 34 0 0 1 24-24" strokeWidth="5" strokeLinecap="round" strokeLinejoin="round" fill="none" className="stroke-brand-200" />
<ellipse cx="175" cy="124" rx="13" ry="15" className="fill-brand-600" />
<ellipse cx="171" cy="124" rx="2.5" ry="4" className="fill-brand-800" />
<ellipse cx="179" cy="124" rx="2.5" ry="4" className="fill-brand-800" />
<circle cx="152" cy="106" r="4.5" className="fill-brand-800" />
<rect x="104" y="80" width="30" height="6" rx="3" className="fill-brand-800" />
<circle cx="119" cy="44" r="17" strokeWidth="3" className="fill-[var(--tst-warning-300)] stroke-[var(--tst-warning-500)]" />
<rect x="115" y="35" width="8" height="18" rx="4" strokeWidth="2.5" fill="none" className="stroke-[var(--tst-warning-500)]" />
<path d="M58 46l3 7 7 3-7 3-3 7-3-7-7-3 7-3z" className="fill-brand-200" />
<path d="M176 66l1.8 4.2 4.2 1.8-4.2 1.8-1.8 4.2-1.8-4.2-4.2-1.8 4.2-1.8z" className="fill-brand-400" />
</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 SavingsOnboarding({ step = 0, 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="mt-5">
<h1 className="text-[26px] font-semibold leading-8 tracking-tight [text-wrap:balance]">{slide.title}</h1>
<p className="mt-2.5 text-[15px] leading-[22px] text-fg-muted">{slide.body}</p>
</div>
<Dots step={step} total={slides.length} className="my-6" />
<button type="button" onClick={onNext} className="h-[52px] w-full rounded-lg bg-primary text-[15px] font-semibold text-primary-fg hover:bg-primary-hover">
{step === slides.length - 1 ? "Get started" : "Next"}
</button>
</main>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.