All Taste components
Animated heroes
Hero — split text and visual (motion)
The week's time slots pop in as a diagonal wave, Thursday 10:30 gets picked, and the booking toast springs up.
copy rises · slot wave · slot pick with ring · toast spring · stacks below 760px
Online booking for small studios
Let clients book themselves while you work.
Slotwise shows your free times, takes the deposit and sends the reminder. You just turn up.
Free for one calendar · Set up in ten minutes
import { ArrowRight, Check } from "lucide-react";
import { cn } from "@/lib/utils";
/* Add once to globals.css:
@keyframes slot-pick { from { background: var(--tst-bg); color: var(--tst-fg-muted); border-color: var(--tst-border); box-shadow: 0 0 0 0 color-mix(in srgb, var(--tst-primary) 45%, transparent); } 70% { box-shadow: 0 0 0 6px transparent; } }
@keyframes slot-pop { from { opacity: 0; scale: .6; } }
@keyframes toast-in { from { opacity: 0; translate: 0 22px; scale: .94; } }
*/
const days = ["Mon", "Tue", "Wed", "Thu", "Fri"];
// One row per time band, one column per day. "taken" slots are crossed out, "picked" is highlighted.
const slots: { time: string; state?: "taken" | "picked" }[] = [
{ time: "9:00" }, { time: "9:00", state: "taken" }, { time: "9:30" }, { time: "10:30", state: "picked" }, { time: "9:00", state: "taken" },
{ time: "11:00" }, { time: "10:00", state: "taken" }, { time: "11:00" }, { time: "11:30" }, { time: "10:30" },
{ time: "13:30" }, { time: "12:00", state: "taken" }, { time: "14:00" }, { time: "14:00" }, { time: "15:00", state: "taken" },
];
export function HeroSplitVisualMotion() {
return (
<section aria-labelledby="hero-title" className="bg-bg font-sans">
<div className="mx-auto grid max-w-6xl items-center gap-10 px-4 py-16 sm:px-6 sm:py-24 lg:grid-cols-[1fr_1.1fr] lg:gap-14">
<div className="flex flex-col items-start gap-5 [&>*]:animate-rise motion-reduce:[&>*]:animate-fade">
<p className="text-[13px] font-medium text-primary">Online booking for small studios</p>
<h1 id="hero-title" className="text-balance text-4xl font-semibold leading-[1.04] tracking-tight text-fg [animation-delay:90ms] sm:text-5xl lg:text-6xl">
Let clients book themselves while you work.
</h1>
<p className="max-w-lg text-pretty text-base leading-relaxed text-fg-muted [animation-delay:180ms] sm:text-lg">
Slotwise shows your free times, takes the deposit and sends the reminder. You just turn up.
</p>
<div className="flex flex-wrap gap-3 [animation-delay:270ms]">
<a href="/signup" className="inline-flex h-11 items-center rounded bg-primary px-5 text-[15px] font-medium text-primary-fg transition hover:bg-primary-hover active:scale-[.97]">Create your booking page</a>
<a href="/example" className="inline-flex h-11 items-center gap-1.5 rounded px-5 text-[15px] font-medium text-fg transition hover:bg-surface-2">
See an example <ArrowRight className="size-4" aria-hidden />
</a>
</div>
<p className="text-[13px] text-fg-subtle [animation-delay:360ms]">Free for one calendar · Set up in ten minutes</p>
</div>
{/* Illustration drawn with plain elements so it follows the theme */}
<div aria-hidden className="relative overflow-hidden rounded-xl bg-surface bg-[radial-gradient(80%_70%_at_100%_0%,color-mix(in_srgb,var(--tst-primary)_34%,transparent),transparent_70%)] p-4 pb-24 animate-pop [animation-delay:150ms] motion-reduce:animate-fade sm:min-h-[400px] sm:p-7">
<div className="absolute -bottom-16 -right-16 size-56 rounded-full border-[40px] border-primary/15" />
<div className="relative max-w-sm rounded-lg border border-border bg-bg p-4 text-[12.5px] shadow-2xl animate-rise [animation-delay:350ms] motion-reduce:animate-fade">
<div className="flex items-center justify-between">
<p className="text-sm font-semibold text-fg">Haircut · 45 min</p>
<p className="text-fg-subtle">Oct 2026</p>
</div>
<div className="mt-3 grid grid-cols-5 gap-1.5 text-center">
{days.map((d) => <p key={d} className="text-[11px] font-medium text-fg-subtle">{d}</p>)}
{slots.map((s, i) => {
// Diagonal wave: row + column decides when a slot pops in.
const wave = 600 + (Math.floor(i / 5) + (i % 5)) * 55 + "ms";
return (
<span
key={i}
style={{ animationDelay: s.state === "picked" ? wave + ", 1450ms" : wave }}
className={cn(
"rounded-sm border border-border py-1.5 tabular-nums text-fg-muted animate-[slot-pop_.45s_cubic-bezier(.34,1.56,.64,1)_both] motion-reduce:animate-fade",
s.state === "taken" && "border-transparent bg-surface-2 text-fg-subtle line-through",
s.state === "picked" && "border-primary bg-primary font-semibold text-primary-fg [animation:slot-pop_.45s_cubic-bezier(.34,1.56,.64,1)_both,slot-pick_.55s_cubic-bezier(.2,.7,.1,1)_both]",
)}
>
{s.time}
</span>
);
})}
</div>
</div>
<div className="absolute inset-x-4 bottom-4 flex items-center gap-3 rounded-lg border border-border bg-bg px-3.5 py-3 text-[12.5px] shadow-xl animate-[toast-in_.7s_cubic-bezier(.34,1.56,.64,1)_1.85s_both] motion-reduce:animate-fade sm:inset-x-auto sm:bottom-8 sm:right-7">
<span className="grid size-7 place-items-center rounded-full bg-success-soft text-success-fg"><Check className="size-3.5" /></span>
<div>
<p className="text-[13px] font-semibold text-fg">Booked: Anaya, Thu 10:30</p>
<p className="text-fg-subtle">₹500 deposit paid</p>
</div>
</div>
</div>
</div>
</section>
);
}
The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.