Skip to content
All Taste components

Animated heroes

Hero — product screenshot (motion)

The app window tilts up from below into place, its figures and invoice rows fill in, and then it floats gently.

copy rises · 3D tilt-in · figures pop · rows slide in · slow float · bottom fade

Now with bank feeds

See every invoice, every client, every payment — on one screen.

Fernbill is the calm dashboard for the money side of freelancing. Know what's due, what's late and what's landed.

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

/* Add once to globals.css:
@keyframes tilt-in { from { opacity: 0; transform: perspective(1600px) rotateX(24deg) translateY(56px) scale(.94); } }
@keyframes bob { from { translate: 0 0; } to { translate: 0 -6px; } }
@keyframes row-in { from { opacity: 0; translate: -10px 0; } }
*/

const nav = [
  { icon: FileText, label: "Invoices", active: true },
  { icon: Users, label: "Clients" },
  { icon: Repeat, label: "Retainers" },
  { icon: BarChart3, label: "Reports" },
  { icon: CreditCard, label: "Payouts" },
];
const figures = [
  { label: "Outstanding", value: "$8,420" },
  { label: "Overdue", value: "$1,150" },
  { label: "Paid this month", value: "$14,900" },
];
const status = {
  paid: "bg-success-soft text-success-fg",
  due: "bg-warning-soft text-warning-fg",
  late: "bg-danger-soft text-danger-fg",
};
const invoices: { client: string; number: string; due: string; state: keyof typeof status }[] = [
  { client: "Kaveri Foods", number: "INV-1042", due: "30 Oct", state: "paid" },
  { client: "Blue Pine Studio", number: "INV-1041", due: "28 Oct", state: "due" },
  { client: "Lumen Health", number: "INV-1040", due: "21 Oct", state: "late" },
  { client: "Orbit Coffee", number: "INV-1039", due: "18 Oct", state: "paid" },
];

export function HeroProductShotMotion() {
  return (
    <section aria-labelledby="hero-title" className="overflow-hidden bg-bg pt-16 font-sans sm:pt-24">
      <div className="mx-auto flex max-w-3xl flex-col items-center gap-5 px-4 text-center sm:px-6 [&>*]:animate-rise motion-reduce:[&>*]:animate-fade">
        <a href="/changelog/bank-feeds" className="inline-flex items-center gap-1 rounded-full border border-border-strong px-2.5 py-0.5 text-xs font-medium text-fg-muted hover:text-fg">
          Now with bank feeds <ChevronRight className="size-3" aria-hidden />
        </a>
        <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">
          See every invoice, every client, every payment — on one screen.
        </h1>
        <p className="max-w-xl text-pretty text-base leading-relaxed text-fg-muted [animation-delay:180ms] sm:text-lg">
          Fernbill is the calm dashboard for the money side of freelancing. Know what’s due, what’s late and what’s landed.
        </p>
        <div className="flex flex-wrap justify-center 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]">Start free</a>
          <a href="/demo" className="inline-flex h-11 items-center rounded border border-border-strong bg-bg px-5 text-[15px] font-medium text-fg transition hover:bg-surface active:scale-[.97]">Book a demo</a>
        </div>
      </div>

      {/* App picture: plain elements, so it follows the theme. Tilts up into place, then floats. */}
      <div aria-hidden className="mx-auto mt-14 max-w-5xl px-4 [mask-image:linear-gradient(to_bottom,black_62%,transparent)] sm:px-6">
        <div className="origin-top overflow-hidden rounded-t-xl border border-b-0 border-border bg-bg shadow-[0_-20px_80px_-30px_var(--tst-primary)] [animation:tilt-in_1.2s_cubic-bezier(.2,.7,.1,1)_.4s_both,bob_7s_ease-in-out_1.8s_infinite_alternate] motion-reduce:animate-fade">
          <div className="flex h-9 items-center gap-1.5 border-b border-border bg-surface px-3.5">
            <span className="size-2.5 rounded-full bg-border-strong" /><span className="size-2.5 rounded-full bg-border-strong" /><span className="size-2.5 rounded-full bg-border-strong" />
            <span className="ml-2.5 h-[18px] w-60 max-w-[40%] rounded-sm bg-surface-2" />
          </div>
          <div className="grid min-h-[380px] text-[12.5px] md:grid-cols-[184px_1fr]">
            <div className="hidden content-start gap-0.5 border-r border-border bg-surface p-2.5 md:grid">
              {nav.map(({ icon: Icon, label, active }) => (
                <span key={label} className={cn("flex items-center gap-2 rounded px-2.5 py-2 text-fg-muted", active && "bg-bg font-medium text-fg shadow-sm")}>
                  <Icon className="size-3.5" /> {label}
                </span>
              ))}
            </div>
            <div className="grid content-start gap-4 p-5">
              <div className="flex items-center justify-between">
                <p className="text-[15px] font-semibold text-fg">Invoices</p>
                <span className="rounded bg-primary px-2.5 py-1.5 text-xs font-medium text-primary-fg">New invoice</span>
              </div>
              <div className="grid gap-2.5 sm:grid-cols-3">
                {figures.map((f, i) => (
                  <div key={f.label} className="rounded-lg border border-border p-3 animate-pop motion-reduce:animate-fade" style={{ animationDelay: 1050 + i * 90 + "ms" }}>
                    <p className="text-fg-subtle">{f.label}</p>
                    <p className="text-lg font-semibold tabular-nums text-fg">{f.value}</p>
                  </div>
                ))}
              </div>
              <div className="overflow-hidden rounded-lg border border-border">
                {invoices.map((inv, i) => (
                  <div
                    key={inv.number}
                    style={{ animationDelay: 1330 + i * 85 + "ms" }}
                    className="grid grid-cols-[1fr_72px] items-center gap-3 border-t border-border px-3 py-2.5 animate-[row-in_.5s_cubic-bezier(.2,.7,.1,1)_both] first:border-t-0 motion-reduce:animate-fade sm:grid-cols-[1.6fr_1fr_0.8fr_72px]"
                  >
                    <span className="truncate text-fg">{inv.client}</span>
                    <span className="hidden text-fg-muted sm:block">{inv.number}</span>
                    <span className="hidden text-fg-muted sm:block">{inv.due}</span>
                    <span className={cn("w-fit rounded-full px-2 py-0.5 text-xs font-medium capitalize", status[inv.state])}>{inv.state}</span>
                  </div>
                ))}
              </div>
            </div>
          </div>
        </div>
      </div>
    </section>
  );
}

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