Skip to content
All Taste components

Animated sections

Bento grid reveal

A feature grid whose tiles rise in one after another, each with a small live detail, and lift under the pointer.

4 tiles · staggered reveal · hover lift · invoice rows · typing caret · sync flow · report bars

Features

Everything between “sent” and “paid”

INV-1042Kestrel StudioPaid
INV-1043Brightwater DesignSent
INV-1044Oakline JoineryScheduled

Recurring invoices

Set the schedule once. Each one goes out on the day, numbered and filed.

Hi Meera, a gentle nudge on INV-1043 — no rush if it’s already on its way

Reminders in your words

Write it once, in your tone. We send it when it’s due.

₹$€£¥AED

Paid in 24 currencies

Your totals stay in your home currency.

Bank feedLedger
Paid
Pending
Overdue

Bank feeds and reports

Payments sync every hour and land matched, so the report is right when you open it.

import { CreditCard, Repeat } from "lucide-react";
import { cn } from "@/lib/utils";

/* Add once to globals.css:
@keyframes tile-in { from { opacity: 0; transform: translateY(18px) scale(.98); } }
@keyframes dash-flow { to { stroke-dashoffset: -10; } }
@keyframes blink { to { visibility: hidden; } }
*/

const invoices = [
  { id: "INV-1042", client: "Kestrel Studio", status: "Paid", tone: "bg-success-soft text-success-fg" },
  { id: "INV-1043", client: "Brightwater Design", status: "Sent", tone: "bg-info-soft text-info-fg" },
  { id: "INV-1044", client: "Oakline Joinery", status: "Scheduled", tone: "bg-surface-2 text-fg-muted" },
];
const currencies = ["₹", "$", "€", "£", "¥", "AED"];
const report = [
  { label: "Paid", value: 82 },
  { label: "Pending", value: 46 },
  { label: "Overdue", value: 14 },
];

function Tile({ index, className, title, text, children }: { index: number; className?: string; title: string; text: string; children: React.ReactNode }) {
  return (
    <article
      // "backwards" fill: after the entrance the hover lift takes over
      className={cn(
        "grid min-h-[190px] content-between gap-4 overflow-hidden rounded-lg border border-border bg-surface p-[18px] transition duration-300 ease-taste hover:-translate-y-[3px] hover:border-border-strong hover:shadow-[0_18px_40px_-26px_rgb(0_0_0/.4)] animate-[tile-in_.7s_cubic-bezier(.2,.7,.1,1)_backwards] motion-reduce:animate-none motion-reduce:transition-none",
        className,
      )}
      style={{ animationDelay: 150 + index * 100 + "ms" }}
    >
      {children}
      <div>
        <h3 className="text-[15px] font-medium tracking-tight text-fg">{title}</h3>
        <p className="mt-1 text-[13.5px] leading-relaxed text-fg-muted">{text}</p>
      </div>
    </article>
  );
}

export function BentoReveal() {
  return (
    <section aria-labelledby="bento-title" className="bg-bg py-20 font-sans">
      <div className="mx-auto max-w-5xl px-4 sm:px-6">
        <p className="text-xs font-medium text-fg-subtle">Features</p>
        <h2 id="bento-title" className="mt-2 max-w-lg text-balance text-3xl font-medium tracking-tight text-fg">Everything between “sent” and “paid”</h2>
        <div className="mt-8 grid gap-3 md:grid-cols-3">
          <Tile index={0} className="md:col-span-2" title="Recurring invoices" text="Set the schedule once. Each one goes out on the day, numbered and filed.">
            <div className="grid gap-1.5">
              {invoices.map((inv, i) => (
                <div key={inv.id} className="flex items-center gap-2.5 rounded border border-border bg-bg px-2.5 py-2 text-[12.5px] animate-rise motion-reduce:animate-none" style={{ animationDelay: 450 + i * 70 + "ms" }}>
                  <span className="tabular-nums text-fg-subtle">{inv.id}</span>
                  <span className="min-w-0 flex-1 truncate text-fg">{inv.client}</span>
                  <span className={cn("rounded-full px-2 py-0.5 text-xs font-medium", inv.tone)}>{inv.status}</span>
                </div>
              ))}
            </div>
          </Tile>
          <Tile index={1} title="Reminders in your words" text="Write it once, in your tone. We send it when it’s due.">
            <p className="justify-self-start rounded-lg rounded-bl-sm border border-border bg-bg px-3 py-2 text-[12.5px] leading-relaxed text-fg">
              Hi Meera, a gentle nudge on INV-1043 — no rush if it’s already on its way
              <span className="ml-0.5 inline-block h-[1em] w-[1.5px] translate-y-0.5 bg-primary animate-[blink_1s_steps(2,start)_infinite] motion-reduce:animate-none" />
            </p>
          </Tile>
          <Tile index={2} title="Paid in 24 currencies" text="Your totals stay in your home currency.">
            <div className="flex flex-wrap gap-1.5">
              {currencies.map((c, i) => (
                <span key={c} className="grid h-[30px] min-w-[38px] place-items-center rounded border border-border bg-bg px-2 text-[12.5px] font-medium text-fg animate-pop motion-reduce:animate-none" style={{ animationDelay: 650 + i * 70 + "ms" }}>
                  {c}
                </span>
              ))}
            </div>
          </Tile>
          <Tile index={3} className="md:col-span-2" title="Bank feeds and reports" text="Payments sync every hour and land matched, so the report is right when you open it.">
            <div className="grid gap-3.5">
              <div className="flex items-center gap-2">
                <span className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border bg-bg px-2.5 text-xs text-fg"><CreditCard className="size-3.5" />Bank feed</span>
                <svg viewBox="0 0 100 12" preserveAspectRatio="none" className="h-3 min-w-10 flex-1" aria-hidden>
                  <line x1="2" y1="6" x2="98" y2="6" className="stroke-primary [stroke-dasharray:4_6] [stroke-linecap:round] [stroke-width:2] animate-[dash-flow_1.1s_linear_infinite] motion-reduce:animate-none" />
                </svg>
                <span className="inline-flex h-[30px] items-center gap-1.5 rounded border border-border bg-bg px-2.5 text-xs text-fg"><Repeat className="size-3.5" />Ledger</span>
              </div>
              <div className="grid gap-2 text-[11.5px] text-fg-muted">
                {report.map((r, i) => (
                  <div key={r.label} className="grid grid-cols-[58px_1fr] items-center gap-2">
                    <span>{r.label}</span>
                    <div className="h-1.5 overflow-hidden rounded-full bg-surface-2">
                      <span className="block h-full origin-left rounded-full bg-primary animate-grow-x motion-reduce:animate-none" style={{ width: r.value + "%", animationDelay: 750 + i * 70 + "ms" }} />
                    </div>
                  </div>
                ))}
              </div>
            </div>
          </Tile>
        </div>
      </div>
    </section>
  );
}

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