A journaling app's eight weeks after its relaunch, climbing a store's Productivity chart from sixth to first, with a rank readout that turns over at each pass.
registry/demos/standings-demo.tsx
"use client";
import * as React from "react";
import { Standings, type StandingsEntry } from "@/components/ui/standings";
// Daybook, a journaling app, over the eight weeks after its relaunch: daily
// downloads in the store's Productivity chart, and the five apps above it.
const WEEKS = ["W1", "W2", "W3", "W4", "W5", "W6", "W7", "W8"];
function AppIcon({ from, to, children }: { from: string; to: string; children: React.ReactNode }) {
return (
<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, ${from}, ${to})` }}
>
<svg viewBox="0 0 20 20" className="size-[18px]" fill="none" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round" aria-hidden>
{children}
</svg>
</span>
);
}
const APPS: StandingsEntry[] = [
{
id: "daybook",
label: "Daybook",
detail: "Journal & mood",
you: true,
value: [3100, 3900, 5200, 6400, 7900, 9300, 10800, 12600],
icon: (
<AppIcon from="#ff8a3d" to="#ff3d00">
<path d="M5 3.5h8.5a1.5 1.5 0 0 1 1.5 1.5v11.5H6.5A1.5 1.5 0 0 1 5 15V3.5Z" />
<path d="M8 7.5h4M8 10.5h2.5" />
</AppIcon>
),
},
{
id: "tally",
label: "Tally Notes",
detail: "Notes & lists",
value: [11800, 11600, 11900, 11400, 11200, 11000, 10900, 10600],
icon: (
<AppIcon from="#4f8bff" to="#1d4ed8">
<path d="M5.5 6h9M5.5 10h9M5.5 14h5.5" />
</AppIcon>
),
},
{
id: "northdesk",
label: "Northdesk",
detail: "Shared inbox",
value: [9800, 9600, 9100, 9400, 9000, 8800, 8600, 8500],
icon: (
<AppIcon from="#34d399" to="#059669">
<path d="M3.5 10.5 6 5h8l2.5 5.5V15h-13v-4.5Z" />
<path d="M3.5 10.5H7l1 2h4l1-2h3.5" />
</AppIcon>
),
},
{
id: "kiln",
label: "Kiln Calendar",
detail: "Calendar & booking",
value: [8600, 8900, 9300, 8200, 7600, 7300, 7100, 6900],
icon: (
<AppIcon from="#fb7185" to="#e11d48">
<rect x="4" y="5" width="12" height="11" rx="2" />
<path d="M4 8.5h12M7.5 3.5v3M12.5 3.5v3" />
</AppIcon>
),
},
{
id: "pebble",
label: "Pebble",
detail: "Habit tracker",
value: [6200, 6000, 5800, 6100, 6300, 6000, 5900, 5700],
icon: (
<AppIcon from="#a78bfa" to="#6d28d9">
<circle cx="10" cy="10" r="5.5" />
<path d="m7.8 10 1.6 1.6 3-3.2" />
</AppIcon>
),
},
{
id: "orbit",
label: "Orbit Focus",
detail: "Focus timer",
value: [4900, 4700, 4500, 4300, 4100, 3900, 3800, 3600],
icon: (
<AppIcon from="#fbbf24" to="#ea580c">
<circle cx="10" cy="10.5" r="5.5" />
<path d="M10 7.5v3l2 1.5M8.5 3.5h3" />
</AppIcon>
),
},
];
export function StandingsDemo() {
const [place, setPlace] = React.useState(6);
const numeral = React.useRef<HTMLSpanElement>(null);
const onOrderChange = React.useCallback((order: string[]) => setPlace(order.indexOf("daybook") + 1), []);
// The numeral rolls in from below whenever Daybook changes place.
const drawn = React.useRef(place);
React.useEffect(() => {
if (drawn.current === place) return;
drawn.current = place;
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) return;
numeral.current?.animate(
[
{ transform: "translateY(40%)", opacity: 0 },
{ transform: "translateY(0)", opacity: 1 },
],
{ duration: 260, easing: "cubic-bezier(0.23, 1, 0.32, 1)" },
);
}, [place]);
return (
<div className="w-full max-w-xl rounded-2xl border border-zinc-950/[0.08] bg-white text-zinc-950 shadow-[0_1px_2px_rgb(0_0_0/0.04),0_12px_32px_-12px_rgb(0_0_0/0.12)] dark:border-white/10 dark:bg-zinc-950 dark:text-zinc-50">
<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]">Top free · Productivity</p>
<p className="mt-0.5 text-xs text-zinc-500 dark:text-zinc-400">United States · daily downloads</p>
</div>
<div className="text-right">
<p className="text-[11px] font-medium text-zinc-500 dark:text-zinc-400">Daybook rank</p>
<p className="mt-0.5 overflow-hidden text-3xl leading-none font-semibold tracking-[-0.04em] tabular-nums">
<span className="text-[#ff5a1f]">#</span>
<span ref={numeral} className="inline-block">
{place}
</span>
</p>
</div>
</div>
<Standings
aria-label="Top free apps in Productivity, by week"
entries={APPS}
periods={WEEKS}
loop
onOrderChange={onOrderChange}
className="px-2 pt-1 pb-3 [--standings-surface:#ffffff] dark:[--standings-surface:#09090b]"
/>
</div>
);
}
Live preview, examples and props: https://keyframeui.com/components/standings#standings-demo