Skip to content
All Taste components

Animated heroes

Hero — left-aligned with stats row (motion)

The divider draws across under the headline and the four figures count up from zero in CSS, one after another.

copy rises · rule draws · CSS count-up (integer, k, decimal, thousands) · 2 × 2 below 760px

Fernbill for studios

The billing desk for teams that would rather be making things.

Quotes, invoices, retainers and reminders in one place — with a clear view of what's owed and when it lands.

invoices sent last month
186k
average days to get paid
9.4
currencies supported
24
studios on Fernbill
12,400
import type { CSSProperties } from "react";

/* Add once to globals.css — the count-up is pure CSS (an animated integer drawn as a counter):
@property --n { syntax: "<integer>"; initial-value: 0; inherits: false; }
@keyframes count-up { from { --n: 0; } }
@keyframes rule-draw { from { transform: scaleX(0); } }
.count { --n: var(--to); }
@supports (width: calc(mod(5px, 2px))) {
  .count { counter-reset: a var(--n); animation: count-up 2s cubic-bezier(.16,1,.3,1) both; }
  .count::before { content: counter(a) var(--suffix, ""); }
  .count > span { position: absolute; width: 1px; height: 1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; }
  .count-dec, .count-thou { counter-reset: a round(down, var(--n) / 10, 1) b mod(var(--n), 10); }
  .count-dec::before { content: counter(a) "." counter(b); }
  .count-thou::before { content: counter(a) "," counter(b) "00"; }
}
@media (prefers-reduced-motion: reduce) { .count { animation: none; } }
Older browsers simply show the figure as text.
*/

// Template figures — replace them with your own, and keep the source somewhere visible.
// "to" is the figure as a whole number: 9.4 → 94 (count-dec), 12,400 → 124 (count-thou, in hundreds).
const stats = [
  { value: "186k", to: 186, kind: "count", suffix: "k", label: "invoices sent last month" },
  { value: "9.4", to: 94, kind: "count count-dec", label: "average days to get paid" },
  { value: "24", to: 24, kind: "count", label: "currencies supported" },
  { value: "12,400", to: 124, kind: "count count-thou", label: "studios on Fernbill" },
];

export function HeroWithStatsMotion() {
  return (
    <section aria-labelledby="hero-title" className="bg-bg font-sans">
      <div className="mx-auto max-w-6xl px-4 py-16 sm:px-6 sm:py-24">
        <div className="grid items-end gap-8 lg:grid-cols-[1.5fr_1fr] lg:gap-14">
          <div className="animate-rise motion-reduce:animate-fade">
            <p className="text-[13px] font-medium text-primary">Fernbill for studios</p>
            <h1 id="hero-title" className="mt-5 text-balance text-4xl font-semibold leading-[1.04] tracking-tight text-fg sm:text-5xl lg:text-6xl">
              The billing desk for teams that would rather be making things.
            </h1>
          </div>
          <div className="grid gap-6 animate-rise [animation-delay:180ms] motion-reduce:animate-fade">
            <p className="text-pretty text-base leading-relaxed text-fg-muted">
              Quotes, invoices, retainers and reminders in one place — with a clear view of what’s owed and when it lands.
            </p>
            <div className="flex flex-wrap gap-3">
              <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="/contact" 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]">Talk to us</a>
            </div>
          </div>
        </div>

        <dl className="relative mt-12 grid grid-cols-2 lg:mt-16 lg:grid-cols-4">
          <span aria-hidden className="absolute inset-x-0 top-0 h-px origin-left bg-border animate-[rule-draw_1.1s_cubic-bezier(.2,.7,.1,1)_.45s_both] motion-reduce:animate-fade" />
          {stats.map((s, i) => (
            <div
              key={s.label}
              style={{ animationDelay: 550 + i * 110 + "ms" }}
              className={[
                "flex flex-col gap-1.5 border-b border-border py-5 animate-rise motion-reduce:animate-fade lg:border-b-0 lg:pb-0 lg:pt-6",
                i % 2 === 1 ? "border-l pl-4" : "pr-4",
                i > 0 ? "lg:border-l lg:pl-6" : "lg:pr-6",
              ].join(" ")}
            >
              <dt className="order-2 text-[13.5px] text-fg-muted">{s.label}</dt>
              <dd className="order-1 text-3xl font-semibold tracking-tight tabular-nums text-fg lg:text-4xl">
                <span
                  className={s.kind}
                  style={{ "--to": s.to, "--suffix": s.suffix ? '"' + s.suffix + '"' : '""', animationDelay: 600 + i * 120 + "ms" } as CSSProperties}
                >
                  <span>{s.value}</span>
                </span>
              </dd>
            </div>
          ))}
        </dl>
      </div>
    </section>
  );
}

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