Skip to content
All Taste components

Animated screens

Splash to home

A launch screen whose logo flies into the app bar while the home feed slides in under it.

logo dock · splash fade · staggered feed · tab bar rise

PantryIndiranagar · in 12 min
This weekendFree delivery
over ₹199

Fresh today

See all
Alphonso mangoes1 kg · ₹349
Baby spinach200 g · ₹45
Cherry tomatoes250 g · ₹60

Buy again

Toned milk500 ml · ₹28
Add
Brown eggs6 pcs · ₹72
Add
// Splash → home: the logo docks into the app bar, then the feed slides in.
// Mobile-first page (React + Tailwind on the Taste preset); maps to React Native via NativeWind.
"use client";

import { useEffect, useRef, useState } from "react";
import { Bell, Home, LayoutGrid, Leaf, MapPin, Plus, Search, ShoppingBag, User } from "lucide-react";
import { cn } from "@/lib/utils";

const products = [
  { name: "Alphonso mangoes", meta: "1 kg · ₹349", tone: "from-[var(--tst-warning-300)] to-[var(--tst-warning-600)]" },
  { name: "Baby spinach", meta: "200 g · ₹45", tone: "from-[var(--tst-success-300)] to-[var(--tst-success-700)]" },
  { name: "Cherry tomatoes", meta: "250 g · ₹60", tone: "from-[var(--tst-danger-300)] to-[var(--tst-danger-600)]" },
];
const again = [
  { name: "Toned milk", meta: "500 ml · ₹28", tone: "from-[var(--tst-info-200)] to-[var(--tst-info-500)]" },
  { name: "Brown eggs", meta: "6 pcs · ₹72", tone: "from-[var(--tst-warning-200)] to-[var(--tst-warning-500)]" },
];

export default function SplashToHome() {
  const mark = useRef<HTMLSpanElement>(null);
  const [ready, setReady] = useState(false);

  useEffect(() => {
    const el = mark.current;
    if (!el || window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
      setReady(true);
      return;
    }
    // FLIP: start the logo big in the middle of the screen, land it in the app bar.
    const r = el.getBoundingClientRect();
    const dx = window.innerWidth / 2 - (r.left + r.width / 2);
    const dy = window.innerHeight * 0.45 - (r.top + r.height / 2);
    const from = "translate(" + dx + "px, " + dy + "px) scale(2.5)";
    const anim = el.animate(
      [{ transform: from }, { transform: from, offset: 0.5 }, { transform: "none" }],
      { duration: 1500, easing: "cubic-bezier(.34,1.56,.64,1)", fill: "both" },
    );
    const t = setTimeout(() => setReady(true), 800);
    return () => {
      anim.cancel();
      clearTimeout(t);
    };
  }, []);

  const enter = (i: number) => ({
    className: ready ? "animate-slide-up motion-reduce:animate-none" : "opacity-0",
    style: { animationDelay: 250 + i * 80 + "ms" },
  });

  return (
    <main className="relative mx-auto flex min-h-dvh max-w-md flex-col overflow-hidden bg-bg font-sans text-fg">
      <header className="flex h-14 items-center gap-2.5 pl-4 pr-1.5">
        <span
          ref={mark}
          aria-hidden
          className={cn(
            "relative z-20 grid size-8 shrink-0 place-items-center rounded-lg transition-colors duration-500 motion-reduce:transition-none",
            ready ? "bg-primary text-primary-fg" : "bg-primary-fg text-primary",
          )}
        >
          <Leaf className="size-[18px]" />
        </span>
        <div className={cn("min-w-0 flex-1 transition duration-500 ease-taste motion-reduce:transition-none", ready ? "opacity-100" : "-translate-x-2.5 opacity-0")}>
          <p className="text-base font-semibold leading-5 tracking-tight">Pantry</p>
          <p className="flex items-center gap-1 text-[11.5px] text-fg-muted"><MapPin className="size-3 text-primary" />Indiranagar · in 12 min</p>
        </div>
        <button type="button" aria-label="Notifications" className="relative grid size-11 place-items-center rounded-full hover:bg-surface-2">
          <Bell className="size-5" />
          <span className="absolute right-3 top-3 size-2 rounded-full bg-danger ring-2 ring-bg" />
        </button>
      </header>

      <div className="flex flex-1 flex-col gap-3.5 px-4 pt-1">
        <label {...enter(0)}>
          <span className="flex h-11 items-center gap-2.5 rounded-lg border border-border bg-surface px-3 text-fg-subtle">
            <Search className="size-[18px]" />
            <input className="min-w-0 flex-1 bg-transparent text-sm text-fg outline-none placeholder:text-fg-subtle" placeholder="Search milk, atta, eggs…" />
          </span>
        </label>
        <div {...enter(1)}>
          <div className="relative grid h-24 content-center overflow-hidden rounded-lg bg-primary-soft px-4 text-primary-soft-fg">
            <p className="text-xs">This weekend</p>
            <p className="text-base font-semibold leading-5">Free delivery<br />over ₹199</p>
            <span aria-hidden className="absolute -bottom-6 -right-3.5 size-24 rounded-full bg-gradient-to-br from-[var(--tst-warning-300)] to-[var(--tst-warning-500)]" />
          </div>
        </div>
        <section {...enter(2)}>
          <div className="flex items-baseline justify-between">
            <h2 className="text-base font-semibold">Fresh today</h2>
            <a href="/fresh" className="text-[13px] font-medium text-primary">See all</a>
          </div>
          <ul className="-mx-4 mt-2.5 flex gap-2.5 overflow-x-auto px-4 pb-1">
            {products.map((p) => (
              <li key={p.name} className="w-28 shrink-0">
                <div className="relative grid h-24 place-items-center rounded-lg bg-surface-2">
                  <span className={cn("size-12 rounded-full bg-gradient-to-br", p.tone)} />
                  <button type="button" aria-label={"Add " + p.name} className="absolute bottom-1.5 right-1.5 grid size-7 place-items-center rounded-full bg-bg text-primary ring-1 ring-border">
                    <Plus className="size-4" />
                  </button>
                </div>
                <p className="mt-1.5 truncate text-[12.5px] font-medium">{p.name}</p>
                <p className="text-[11.5px] tabular-nums text-fg-muted">{p.meta}</p>
              </li>
            ))}
          </ul>
        </section>
        <section {...enter(3)}>
          <h2 className="text-base font-semibold">Buy again</h2>
          {again.map((r) => (
            <div key={r.name} className="mt-2.5 flex items-center gap-2.5">
              <span className="grid size-11 shrink-0 place-items-center rounded bg-surface-2"><span className={cn("size-[26px] rounded-full bg-gradient-to-br", r.tone)} /></span>
              <div className="min-w-0 flex-1">
                <p className="text-[13px] font-medium">{r.name}</p>
                <p className="text-[11.5px] tabular-nums text-fg-muted">{r.meta}</p>
              </div>
              <button type="button" className="h-[30px] rounded border border-primary px-3 text-[12.5px] font-semibold text-primary hover:bg-primary-soft">Add</button>
            </div>
          ))}
        </section>
      </div>

      <nav aria-label="Main" className={cn("grid grid-cols-4 border-t border-border bg-bg px-1.5 pb-6 pt-1 transition-transform duration-500 ease-taste motion-reduce:transition-none", !ready && "translate-y-full")}>
        {[["Home", Home], ["Categories", LayoutGrid], ["Orders", ShoppingBag], ["Account", User]].map(([label, Icon], i) => {
          const I = Icon as typeof Home;
          return (
            <a key={label as string} href="#" aria-current={i === 0 ? "page" : undefined} className={cn("flex min-h-11 flex-col items-center justify-center gap-0.5 text-[10.5px] font-medium", i === 0 ? "text-primary" : "text-fg-subtle")}>
              <I className="size-[22px]" />
              {label as string}
            </a>
          );
        })}
      </nav>

      {/* Splash: fades away while the logo docks */}
      <div aria-hidden className={cn("pointer-events-none absolute inset-0 z-10 grid place-items-center bg-primary transition-opacity duration-500 motion-reduce:transition-none", ready && "opacity-0")}>
        <p className="mt-[110px] text-sm font-medium text-primary-fg">Groceries in 12 minutes</p>
      </div>
    </main>
  );
}

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