About
Several models answer the same prompt side by side: time to first token, tokens per second and places as each one finishes, with the winner flagged. Pick the answer you want to keep.
Built with React and Tailwind. Copy the prompt and paste it into Claude Code, Cursor or Lovable: your agent adds Multi Model Race to your project and fits it to your theme.
Code
How Multi Model Race is used: the demo's code. The component's own source comes with a free account.
Usage.tsx
"use client"
import * as React from "react"
import { Button, MultiModelRace, RaceLane } from "@/components/ui/multi-model-race"
const MODELS = [
{ id: "sonnet", model: "Claude Sonnet 5.5", detail: "reasoning low", rate: 96, first: 420, total: 160 },
{ id: "haiku", model: "Claude Haiku 5.5", detail: "default", rate: 170, first: 240, total: 148 },
{ id: "llama", model: "Llama 4 Maverick", detail: "self hosted", rate: 62, first: 900, total: 171 },
{ id: "gpt", model: "GPT-6.1 Sol", detail: "eu region", rate: 120, first: 610, total: 155 },
]
/** Every lane at time `t` into the race. */
function lanesAt(t: number, answer: string): RaceLane[] {
return MODELS.map((m) => {
const tokens = Math.min(m.total, Math.floor((Math.max(0, t - m.first) / 1000) * m.rate))
const done = tokens >= m.total
return {
id: m.id,
model: m.model,
detail: m.detail,
status: t < m.first ? "waiting" : done ? "done" : "streaming",
tokens,
ms: done ? m.first + (m.total / m.rate) * 1000 : t,
firstTokenMs: t >= m.first ? m.first : undefined,
text: answer.slice(0, Math.round((tokens / m.total) * answer.length)),
}
})
}
const QUESTIONS = [
{
ask: "When is the last ferry from the north pier?",
answer:
"Ferries stop at 23:40 on weekdays. The last one from the north pier waits for the late train, so if you are on the 23:12 you will still make it. On Sundays the last ferry leaves at 22:15 and does not wait.",
},
{
ask: "Can I bring a bike on the morning boat?",
answer:
"Yes, up to six bikes ride free on every boat before 10:00. Roll it on at the stern ramp and lock it to the rail; folding bikes can come inside the cabin all day.",
},
]
/** How long a choice holds a tour before it carries on. */
const RESUME_MS = 5000
const reducedMotion = () => window.matchMedia("(prefers-reduced-motion: reduce)").matches
/** How long the slowest lane takes, so the race can hold on the result before the next one. */
const RACE_MS = Math.max(...MODELS.map((m) => m.first + (m.total / m.rate) * 1000))
const FIT_CSS = "html,body{overflow:hidden}[data-fit-small] [data-fit-hide]{display:none!important}"
/**
* 21st shows a demo in a small frame: about 674 by 421 on a component page, 358 by 224 on a
* phone and 245 by 139 on a profile card. A demo taller than its frame gets scrollbars, which
* render white on dark pages. FitFrame fills the frame exactly instead.
*
* - "scale": the content shrinks until it fits, centered. For visuals such as orbs.
* - "crop": the content stays at full size from the top and fades out at the bottom. For text
* that streams in, where a changing scale would make the page breathe.
*
* `minWidth` lays out a wide demo (a table, a two column panel) at that width and scales it down
* to the frame, instead of letting it collapse to a narrow layout. The frame centers it, so it
* shrinks evenly on both sides.
*
* Anything marked `data-fit-hide` (state buttons, the chatcn.dev line) drops out in frames
* shorter than `hideBelow`, so a profile card shows the visual alone.
*/
function FitFrame({
children,
mode = "scale",
hideBelow = 360,
minWidth,
}: {
children: React.ReactNode
mode?: "scale" | "crop"
hideBelow?: number
minWidth?: string
}) {
const content = React.useRef<HTMLDivElement>(null)
const [cropped, setCropped] = React.useState(false)
React.useLayoutEffect(() => {
const el = content.current
if (!el) return
const measure = () => {
const vw = window.innerWidth
const vh = window.innerHeight
el.toggleAttribute("data-fit-small", vh < hideBelow)
// Layout sizes ignore the transform, so measuring never feeds back.
const w = Math.max(el.offsetWidth, el.scrollWidth)
const h = Math.max(el.offsetHeight, el.scrollHeight)
const scale = mode === "scale" ? Math.min(1, vw / w, vh / h) : 1
el.style.transform = scale < 1 ? `scale(${scale})` : ""
setCropped(mode === "crop" && h > vh + 1)
}
measure()
const observer = new ResizeObserver(measure)
observer.observe(el)
// Content that changes inside a fixed width box (a headline that cycles words) never resizes
// the box itself, so changes below it measure again on the next frame.
let frame = 0
const mutations = new MutationObserver(() => {
cancelAnimationFrame(frame)
frame = requestAnimationFrame(measure)
})
mutations.observe(el, { subtree: true, childList: true, characterData: true })
window.addEventListener("resize", measure)
return () => {
observer.disconnect()
mutations.disconnect()
cancelAnimationFrame(frame)
window.removeEventListener("resize", measure)
}
}, [mode, hideBelow])
const fade = "linear-gradient(to bottom, #000 calc(100% - 40px), transparent)"
return (
<div
className="flex w-full justify-center overflow-hidden"
style={{
height: "100dvh",
alignItems: mode === "crop" ? "flex-start" : "center",
maskImage: cropped ? fade : undefined,
WebkitMaskImage: cropped ? fade : undefined,
}}
>
<style>{FIT_CSS}</style>
<div
ref={content}
className="w-full shrink-0"
style={{ transformOrigin: mode === "crop" ? "top center" : "center", minWidth }}
>
{children}
</div>
</div>
)
}
/** The line under each 21st demo that points back to the kit. */
function MoreOn({ slug, children }: { slug: string; children: React.ReactNode }) {
return (
<p data-fit-hide className="text-[0.75rem]/[1rem] text-(color:--chatcn-fg-faint) text-center text-balance">
{children}{" "}
<a
href={`https://chatcn.dev/docs/components/${slug}`}
target="_blank"
rel="noopener"
className="text-(color:--chatcn-fg-subtle) hover:text-(color:--chatcn-fg) underline underline-offset-2"
>
chatcn.dev
</a>
</p>
)
}
/**
* Four models answer the same question at once; places and the winner's flag appear as each one
* finishes, the fastest answer is picked, then the next question starts. Run again replays; a click holds the tour
* for a few seconds.
*/
export default function MultiModelRacePreview() {
const [run, setRun] = React.useState(0)
const [lanes, setLanes] = React.useState<RaceLane[]>(() => lanesAt(0, QUESTIONS[0]!.answer))
const [picked, setPicked] = React.useState<string>()
// Once someone runs or picks, the tour stops; a ref, so stopping never restarts the race.
const auto = React.useRef(true)
const resume = React.useRef(0)
/** Stops the tour, then starts it again from the top a few seconds later. */
const hold = () => {
auto.current = false
window.clearTimeout(resume.current)
resume.current = window.setTimeout(() => {
auto.current = true
setRun((r) => r + 1)
}, RESUME_MS)
}
React.useEffect(() => () => window.clearTimeout(resume.current), [])
const question = QUESTIONS[run % QUESTIONS.length] ?? QUESTIONS[0]!
React.useEffect(() => {
if (reducedMotion()) {
setLanes(lanesAt(RACE_MS, question.answer))
return
}
setPicked(undefined)
const began = performance.now()
let timer = 0
const tick = () => {
const t = performance.now() - began
setLanes(lanesAt(t, question.answer))
if (t < RACE_MS) timer = window.setTimeout(tick, 60)
else if (auto.current) {
// Pick the first to finish, hold on it, then race the next question.
const winner = [...MODELS].sort(
(a, b) => a.first + (a.total / a.rate) * 1000 - (b.first + (b.total / b.rate) * 1000),
)[0]
timer = window.setTimeout(() => {
setPicked(winner?.id)
timer = window.setTimeout(() => setRun((r) => r + 1), 1300)
}, 350)
}
}
timer = window.setTimeout(tick, 0)
return () => window.clearTimeout(timer)
}, [run, question.answer])
const running = lanes.some((l) => l.status !== "done")
return (
<FitFrame>
<div className="flex w-full flex-col items-center justify-center gap-6 p-6">
{/* Tall enough for four finished lanes, so the frame never rescales between races. */}
<div className="flex min-h-[34rem] w-full max-w-2xl flex-col gap-4">
<div className="flex items-center justify-between gap-3">
<p className="text-[0.875rem]/[1.25rem] tracking-[-0.003em] text-(color:--chatcn-fg) truncate">{question.ask}</p>
<Button
variant="outline"
size="sm"
disabled={running}
onClick={() => {
hold()
setRun((r) => r + 1)
}}
>
Run again
</Button>
</div>
<MultiModelRace
lanes={lanes}
onSelect={(id) => {
hold()
setPicked(id)
}}
selectedId={picked}
/>
</div>
<MoreOn slug="multi-model-race">
Time to first token, tokens per second, places as they finish. On
</MoreOn>
</div>
</FitFrame>
)
}















