Skip to content
All Taste components

Animated heroes

Hero, centered (motion)

The faint grid drifts slowly upward behind the copy, the glow breathes, and a single shine sweeps across the main button.

grid drift · breathing glow · pill drops in · copy rises · one CTA shine

NewRecurring invoices

Get paid on time, without the awkward follow-ups.

Fernbill sends your invoices, reminds late clients in your own words and tells you the moment the money lands.

Free for your first 5 invoices a month · No card needed

import { ArrowRight } from "lucide-react";

/* Add once to globals.css:
@keyframes grid-pan { to { transform: translateY(40px); } }
@keyframes glow-breathe { to { opacity: .55; } }
@keyframes drop-in { from { opacity: 0; transform: translateY(-14px); } }
@keyframes shine { from { transform: translateX(-110%); } to { transform: translateX(110%); } }
*/

const hero = {
  announcement: { tag: "New", label: "Recurring invoices", href: "/changelog/recurring" },
  title: "Get paid on time, without the awkward follow-ups.",
  lead: "Fernbill sends your invoices, reminds late clients in your own words and tells you the moment the money lands.",
  primary: { label: "Start free", href: "/signup" },
  secondary: { label: "See how it works", href: "/tour" },
  note: "Free for your first 5 invoices a month · No card needed",
};

export function HeroCenteredMotion() {
  return (
    <section aria-labelledby="hero-title" className="relative isolate overflow-hidden bg-bg font-sans">
      {/* Backdrop: a faint grid drifting up one cell at a time (seamless), under a fixed fade mask */}
      <div aria-hidden className="absolute inset-0 -z-10 overflow-hidden opacity-70 [mask-image:radial-gradient(70%_60%_at_50%_20%,var(--tst-fg),transparent_75%)]">
        <div className="absolute inset-x-0 -top-10 bottom-0 [background-image:linear-gradient(var(--tst-border)_1px,transparent_1px),linear-gradient(90deg,var(--tst-border)_1px,transparent_1px)] [background-size:40px_40px] animate-[grid-pan_10s_linear_infinite] motion-reduce:animate-none" />
      </div>
      <div
        aria-hidden
        className="absolute inset-0 -z-10 bg-[radial-gradient(55%_45%_at_50%_0%,color-mix(in_srgb,var(--tst-primary)_18%,transparent),transparent_70%)] animate-[glow-breathe_6s_ease-in-out_infinite_alternate] motion-reduce:animate-none"
      />

      <div className="mx-auto flex max-w-3xl flex-col items-center px-4 py-20 text-center sm:px-6 sm:py-28">
        <a
          href={hero.announcement.href}
          className="inline-flex max-w-full items-center gap-2 rounded-full border border-border bg-bg py-0.5 pl-0.5 pr-3 text-[13px] text-fg-muted transition hover:border-border-strong animate-[drop-in_.7s_cubic-bezier(.34,1.56,.64,1)_.1s_both] motion-reduce:animate-fade"
        >
          <span className="rounded-full bg-primary-soft px-2 py-0.5 text-xs font-medium text-primary-soft-fg">{hero.announcement.tag}</span>
          {hero.announcement.label}
          <ArrowRight className="size-3.5" aria-hidden />
        </a>

        <h1 id="hero-title" className="mt-6 text-balance text-4xl font-semibold leading-[1.08] tracking-tight text-fg animate-rise [animation-delay:170ms] motion-reduce:animate-fade sm:text-5xl lg:text-6xl">
          {hero.title}
        </h1>
        <p className="mt-5 max-w-xl text-pretty text-base leading-relaxed text-fg-muted animate-rise [animation-delay:260ms] motion-reduce:animate-fade sm:text-lg">{hero.lead}</p>

        <div className="mt-8 flex flex-wrap justify-center gap-3 animate-rise [animation-delay:350ms] motion-reduce:animate-fade">
          {/* One shine sweeps across the main button, once, after the page settles */}
          <a href={hero.primary.href} className="relative isolate inline-flex h-11 items-center gap-1.5 overflow-hidden rounded bg-primary px-5 text-[15px] font-medium text-primary-fg transition hover:bg-primary-hover active:scale-[.97]">
            <span aria-hidden className="pointer-events-none absolute inset-0 -z-10 -translate-x-[110%] bg-[linear-gradient(105deg,transparent_30%,color-mix(in_srgb,var(--tst-primary-fg)_45%,transparent)_50%,transparent_70%)] animate-[shine_1.3s_cubic-bezier(.45,0,.25,1)_1.3s_both] motion-reduce:hidden" />
            {hero.primary.label} <ArrowRight className="size-4" aria-hidden />
          </a>
          <a href={hero.secondary.href} 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]">
            {hero.secondary.label}
          </a>
        </div>
        <p className="mt-5 text-[13px] text-fg-subtle animate-rise [animation-delay:440ms] motion-reduce:animate-fade">{hero.note}</p>
      </div>
    </section>
  );
}

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