# Standings Hero

> 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.

A shadcn/ui block from the [@keyframeui](https://keyframeui.com/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/keyframeui/standings-hero).

- Page, with a live preview: https://shadcn.exchange/r/keyframeui/standings-hero
- Screenshot: https://shadcn.exchange/shot/keyframeui/standings-hero/light.png
- Registry item: https://keyframeui.com/r/standings-hero.json
- Categories: blocks, hero-sections
- Listed: 2026-11-02, source last changed 2026-11-02

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/keyframeui/standings-hero.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- Registry items: `https://keyframeui.com/r/standings.json`

## Notes from the registry

Live preview and the components it's built with: https://keyframeui.com/blocks/hero-sections/standings-hero

## Files

### `registry/blocks/standings-hero.tsx`

```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>
  );
}
```
