A hero for an app-ranking tool. Beside a headline with two boxed words and a link field joined to its button, a store chart races through eight weeks: the visitor's app overtakes one row after another on its way from sixth to first, and the rank readout above it rolls over at each pass. With a navbar.
registry/blocks/standings-hero.tsx
"use client";
import * as React from "react";
import { Standings, type StandingsEntry } from "@/components/ui/standings";
/* ── Content ─────────────────────────────────────────────────
* Everything you'd change to make the hero yours: the nav, the
* headline and its two boxed words, the link field, and the
* chart the product card races through. Your app is the entry
* with `you: true`; the rank readout above the chart follows
* it as it overtakes the others.
* ─────────────────────────────────────────────────────────── */
/** The one accent: buttons, the "#", your row and the timeline. */
const ACCENT = "#ff5a1f";
const BRAND = { name: "Rung", href: "#" };
const NAV_LINKS = [
{ label: "Keywords", href: "#keywords" },
{ label: "Pricing", href: "#pricing" },
{ label: "Changelog", href: "#changelog" },
];
const SIGN_IN = { label: "Sign in", href: "#sign-in" };
const NAV_CTA = { label: "Start free", href: "#start" };
const BADGE = { tag: "New", text: "Rank alerts in Slack", href: "#changelog" };
/** Read as one sentence, in three lines: "Climb to #1 / in Productivity / and stay there." */
const HEADLINE = { lead: "Climb to", rank: "#1", joint: "in", category: "Productivity", tail: "and stay there." };
const SUBLINE =
"Rung checks where you rank for every keyword, in every store and country, every hour. Then it writes the listing changes that move you up.";
const FIELD = { label: "Your app's store link", placeholder: "Paste your app's link", button: "Check my rank" };
const PROOF = { text: "Used by 2,400 app teams", link: { label: "Talk to us", href: "#contact" } };
const AVATARS = [
"https://images.unsplash.com/photo-1494790108377-be9c29b29330?auto=format&fit=facearea&facepad=2.5&w=96&h=96&q=80",
"https://images.unsplash.com/photo-1500648767791-00dcc994a43e?auto=format&fit=facearea&facepad=2.5&w=96&h=96&q=80",
"https://images.unsplash.com/photo-1534528741775-53994a69daeb?auto=format&fit=facearea&facepad=2.5&w=96&h=96&q=80",
];
const CHART = {
title: "Top free · Productivity",
subtitle: "United States · daily downloads",
readout: "Fieldnote rank",
periods: ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"],
};
type App = { id: string; label: string; detail: string; from: string; to: string; glyph: React.ReactNode; value: number[]; you?: boolean };
const APPS: App[] = [
{
id: "fieldnote",
label: "Fieldnote",
detail: "Notes that file themselves",
you: true,
from: "#ff8a3d",
to: "#ff3d00",
glyph: <path d="M6 3.5h8a1.5 1.5 0 0 1 1.5 1.5v10a1.5 1.5 0 0 1-1.5 1.5H6A1.5 1.5 0 0 1 4.5 15V5A1.5 1.5 0 0 1 6 3.5ZM7.5 8h5M7.5 11h3" />,
value: [2900, 3800, 5100, 6500, 8000, 9400, 10900, 12800],
},
{
id: "ledgerly",
label: "Ledgerly",
detail: "Invoices & time tracking",
from: "#4f8bff",
to: "#1d4ed8",
glyph: <path d="M5 4.5h10v11l-2-1.2-1.5 1.2-1.5-1.2-1.5 1.2L7 14.3l-2 1.2v-11ZM7.5 8h5M7.5 10.5h5" />,
value: [11900, 11700, 12000, 11500, 11300, 11100, 11000, 10700],
},
{
id: "stack",
label: "Stack Tasks",
detail: "To-dos for small teams",
from: "#34d399",
to: "#059669",
glyph: <path d="M4.5 6.5 7 9l3.5-4M4.5 13 7 15.5 10.5 11.5M12.5 7h3M12.5 13.5h3" />,
value: [9900, 9700, 9200, 9500, 9100, 8900, 8700, 8600],
},
{
id: "quarry",
label: "Quarry Mail",
detail: "Email, triaged for you",
from: "#fb7185",
to: "#e11d48",
glyph: <path d="M3.5 6.5 10 11l6.5-4.5M4.5 5h11a1 1 0 0 1 1 1v8a1 1 0 0 1-1 1h-11a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1Z" />,
value: [8700, 9000, 9400, 8300, 7700, 7400, 7200, 7000],
},
{
id: "lumen",
label: "Lumen Focus",
detail: "Timers & site blockers",
from: "#a78bfa",
to: "#6d28d9",
glyph: <path d="M10 4.5a5.5 5.5 0 1 0 0 11 5.5 5.5 0 0 0 0-11ZM10 7v3l2 1.5" />,
value: [6300, 6100, 5900, 6200, 6400, 6100, 6000, 5800],
},
{
id: "papertrail",
label: "Papertrail",
detail: "Scan & sign PDFs",
from: "#fbbf24",
to: "#ea580c",
glyph: <path d="M6.5 3.5h5l3 3v9a1 1 0 0 1-1 1h-7a1 1 0 0 1-1-1v-11a1 1 0 0 1 1-1ZM11.5 3.5v3h3M8 11.5l1.5 1.5 3-3" />,
value: [5000, 4800, 4600, 4400, 4200, 4000, 3900, 3700],
},
];
const numbers = new Intl.NumberFormat("en-US");
/* ── Section ─────────────────────────────────────────────── */
export function StandingsHero() {
const you = APPS.find((app) => app.you)?.id;
const [place, setPlace] = React.useState(() => {
const first = [...APPS].sort((a, b) => b.value[0] - a.value[0]);
return first.findIndex((app) => app.id === you) + 1;
});
const onOrderChange = React.useCallback((ids: string[]) => setPlace(ids.indexOf(you ?? "") + 1), [you]);
// The readout turns over at each pass: the new rank rises in from below.
const numeral = React.useRef<HTMLSpanElement>(null);
const shown = React.useRef(place);
React.useEffect(() => {
if (shown.current === place) return;
const up = place < shown.current;
shown.current = place;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
numeral.current?.animate(
[
{ transform: `translateY(${up ? 60 : -60}%)`, opacity: 0 },
{ transform: "translateY(0)", opacity: 1 },
],
{ duration: 280, easing: "cubic-bezier(0.23, 1, 0.32, 1)" },
);
}, [place]);
const entries = React.useMemo<StandingsEntry[]>(
() =>
APPS.map((app) => ({
id: app.id,
label: app.label,
detail: app.detail,
you: app.you,
value: app.value,
icon: (
<span
className="grid size-full place-items-center text-white shadow-[inset_0_0_0_1px_rgb(255_255_255/0.18)]"
style={{ backgroundImage: `linear-gradient(145deg, ${app.from}, ${app.to})` }}
>
<svg viewBox="0 0 20 20" className="size-[18px]" fill="none" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
{app.glyph}
</svg>
</span>
),
})),
[],
);
return (
<section
style={{ "--accent": ACCENT } as React.CSSProperties}
className="relative bg-[#f5f5f6] px-3 pt-3 pb-14 text-zinc-950 sm:px-4 sm:pb-20 dark:bg-[#09090b] dark:text-white"
>
<header className="sticky top-3 z-30 mx-auto flex h-14 max-w-6xl items-center justify-between rounded-2xl bg-white pr-2.5 pl-5 shadow-[0_1px_2px_rgb(0_0_0/0.05),0_0_0_1px_rgb(0_0_0/0.05)] dark:bg-[#141416] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.08)]">
<a href={BRAND.href} className="flex items-center gap-2 rounded-md text-[15px] font-semibold tracking-tight focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent)">
<RungMark className="size-5 text-(--accent)" />
{BRAND.name}
</a>
<nav className="hidden items-center gap-7 text-sm text-zinc-600 md:flex dark:text-zinc-400">
{NAV_LINKS.map((link) => (
<a key={link.label} href={link.href} className="rounded-sm transition-colors duration-150 hover:text-zinc-950 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-(--accent) dark:hover:text-white">
{link.label}
</a>
))}
</nav>
<div className="flex items-center gap-1.5">
<a href={SIGN_IN.href} className="hidden rounded-full px-3 py-1.5 text-sm font-medium text-zinc-700 transition-colors duration-150 hover:text-zinc-950 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) sm:block dark:text-zinc-300 dark:hover:text-white">
{SIGN_IN.label}
</a>
<a href={NAV_CTA.href} className="flex items-center gap-1 rounded-full bg-(--accent) py-1.5 pr-3 pl-3.5 text-sm font-semibold text-white transition-[filter] duration-150 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent)">
{NAV_CTA.label}
<Chevron className="size-3.5" />
</a>
</div>
</header>
<div className="mx-auto mt-3 max-w-6xl rounded-[28px] bg-white px-6 py-12 shadow-[0_1px_2px_rgb(0_0_0/0.04),0_0_0_1px_rgb(0_0_0/0.04),0_24px_60px_-30px_rgb(0_0_0/0.18)] sm:px-10 lg:py-14 lg:pr-8 lg:pl-14 dark:bg-[#111113] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.07)]">
<div className="grid items-center gap-x-12 gap-y-12 lg:grid-cols-[minmax(0,1fr)_minmax(0,1.02fr)]">
<div>
<a
href={BADGE.href}
className="inline-flex items-center gap-2 rounded-full bg-zinc-950/[0.04] py-1 pr-3 pl-1 text-xs font-medium text-zinc-700 ring-1 ring-zinc-950/[0.06] transition-colors duration-150 hover:bg-zinc-950/[0.07] focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) dark:bg-white/[0.06] dark:text-zinc-300 dark:ring-white/10"
>
<span className="rounded-full bg-zinc-950 px-2 py-0.5 text-white dark:bg-white dark:text-zinc-950">{BADGE.tag}</span>
{BADGE.text}
<Chevron className="size-3 text-zinc-400" />
</a>
<h1 className="mt-6 text-[2.3rem] leading-[1.1] font-semibold tracking-[-0.04em] sm:text-[3.4rem] lg:text-[3.75rem]">
<span className="whitespace-nowrap">
{HEADLINE.lead}{" "}
<span className="inline-flex translate-y-[-0.05em] items-center rounded-[0.3em] border border-zinc-950/10 bg-white px-[0.2em] leading-[1.14] text-(--accent) shadow-[0_1px_2px_rgb(0_0_0/0.05)] dark:border-white/12 dark:bg-white/[0.04]">
{HEADLINE.rank}
</span>
</span>{" "}
<span className="whitespace-nowrap">
{HEADLINE.joint}{" "}
<span className="inline-flex translate-y-[-0.05em] items-center gap-[0.18em] rounded-[0.3em] border border-zinc-950/10 bg-white py-[0.02em] pr-[0.22em] pl-[0.14em] leading-[1.14] shadow-[0_1px_2px_rgb(0_0_0/0.05)] dark:border-white/12 dark:bg-white/[0.04]">
<CategoryGlyph className="size-[0.78em] shrink-0" />
{HEADLINE.category}
</span>
</span>{" "}
<span className="text-zinc-400 dark:text-zinc-500">{HEADLINE.tail}</span>
</h1>
<p className="mt-6 max-w-[30rem] text-base leading-relaxed text-pretty text-zinc-600 sm:text-[17px] dark:text-zinc-400">{SUBLINE}</p>
<form
onSubmit={(e) => e.preventDefault()}
className="mt-8 flex max-w-[30rem] items-center gap-2 rounded-full bg-white p-1.5 pl-4 shadow-[0_1px_2px_rgb(0_0_0/0.05)] ring-1 ring-zinc-950/10 focus-within:ring-2 focus-within:ring-(--accent) dark:bg-white/[0.04] dark:ring-white/12"
>
<LinkGlyph className="size-4 shrink-0 text-zinc-400" />
<label htmlFor="rung-store-link" className="sr-only">
{FIELD.label}
</label>
<input
id="rung-store-link"
type="url"
inputMode="url"
placeholder={FIELD.placeholder}
className="h-10 min-w-0 flex-1 bg-transparent text-[15px] outline-none placeholder:text-zinc-400 dark:placeholder:text-zinc-500"
/>
<button
type="submit"
className="flex h-10 shrink-0 items-center gap-1 rounded-full bg-(--accent) pr-3.5 pl-4 text-sm font-semibold text-white transition-[filter,transform] duration-150 hover:brightness-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) active:scale-[0.97]"
>
{FIELD.button}
<Chevron className="size-3.5" />
</button>
</form>
<div className="mt-5 flex items-center gap-3 text-sm text-zinc-500 dark:text-zinc-400">
<span className="flex -space-x-2">
{AVATARS.map((src) => (
// eslint-disable-next-line @next/next/no-img-element
<img key={src} src={src} alt="" className="size-7 rounded-full object-cover ring-2 ring-white dark:ring-[#111113]" />
))}
</span>
<span>
{PROOF.text} ·{" "}
<a href={PROOF.link.href} className="font-medium text-zinc-950 underline decoration-zinc-950/20 underline-offset-4 transition-colors duration-150 hover:decoration-(--accent) focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-(--accent) dark:text-white dark:decoration-white/25">
{PROOF.link.label}
</a>
</span>
</div>
</div>
<div className="rounded-[22px] bg-[#f5f5f6] p-2 ring-1 ring-zinc-950/[0.04] sm:p-3 dark:bg-black/40 dark:ring-white/[0.05]">
<div className="rounded-2xl bg-white shadow-[0_1px_2px_rgb(0_0_0/0.05),0_0_0_1px_rgb(0_0_0/0.06),0_16px_32px_-20px_rgb(0_0_0/0.25)] dark:bg-[#18181b] dark:shadow-[0_0_0_1px_rgb(255_255_255/0.08)]">
<div className="flex items-start justify-between gap-4 border-b border-zinc-950/[0.07] px-5 pt-4 pb-3.5 dark:border-white/[0.08]">
<div className="min-w-0">
<p className="text-[13px] font-semibold tracking-[-0.01em]">{CHART.title}</p>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">{CHART.subtitle}</p>
</div>
<div className="text-right">
<p className="text-[11px] font-medium text-zinc-500 dark:text-zinc-400">{CHART.readout}</p>
<p className="mt-1 flex items-baseline justify-end overflow-hidden py-0.5 text-3xl leading-none font-semibold tracking-[-0.04em]">
<span className="text-(--accent)">#</span>
<span ref={numeral} className="inline-block tabular-nums">
{place}
</span>
</p>
</div>
</div>
<Standings
aria-label={`${CHART.title}, ${CHART.subtitle}, by week`}
entries={entries}
periods={CHART.periods}
loop
format={(v) => numbers.format(Math.round(v))}
onOrderChange={onOrderChange}
className="px-2 pt-1 pb-3 [--standings-accent:var(--accent)] [--standings-surface:#ffffff] dark:[--standings-surface:#18181b]"
/>
</div>
</div>
</div>
</div>
</section>
);
}
/* ── Marks ───────────────────────────────────────────────── */
function RungMark({ className }: { className?: string }) {
return (
<svg viewBox="0 0 20 20" className={className} fill="none" stroke="currentColor" strokeWidth="2.2" strokeLinecap="round" aria-hidden>
<path d="M5 17.5v-15M15 17.5v-15M5 13.5h10M5 8.5h10" />
</svg>
);
}
function CategoryGlyph({ className }: { className?: string }) {
return (
<svg viewBox="0 0 24 24" className={className} aria-hidden>
<defs>
<linearGradient id="rung-category" x1="0" y1="0" x2="1" y2="1">
<stop offset="0" stopColor="#ff8a3d" />
<stop offset="1" stopColor="#ff3d00" />
</linearGradient>
</defs>
<rect width="24" height="24" rx="6" fill="url(#rung-category)" />
<path d="m7 12.2 3 3 7-7.4" fill="none" stroke="#fff" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" />
</svg>
);
}
function LinkGlyph({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden>
<path d="M6.8 9.2a2.6 2.6 0 0 0 3.7 0l2.2-2.2a2.6 2.6 0 0 0-3.7-3.7l-.9.9M9.2 6.8a2.6 2.6 0 0 0-3.7 0L3.3 9a2.6 2.6 0 0 0 3.7 3.7l.9-.9" />
</svg>
);
}
function Chevron({ className }: { className?: string }) {
return (
<svg viewBox="0 0 16 16" className={className} fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
<path d="m6 4 4 4-4 4" />
</svg>
);
}
Live preview and the components it's built with: https://keyframeui.com/blocks/hero-sections/standings-hero
