Xzero UI
Pro library

Model arenaPro

A blind side by side model comparison in one card: send one prompt to two models, watch both answers stream at the same pace, vote for the better one, then reveal which model wrote each.

$ pnpm dlx shadcn@4.23.0 add @xzero-ui-pro/model-arena
Live · keyboard ready

Use it for evals, demos or a feedback page. Pass models and respond to stream real answers per side, and onVote to record the choice; with no props it plays scripted answers.

When to use

When not to use

Installation

Register both Xzero UI namespaces in your project's components.json:

{
  "registries": {
    "@xzero-ui": "https://mcp.xzero.co.uk/r/{name}.json",
    "@xzero-ui-pro": "https://mcp.xzero.co.uk/r/pro/{name}.json"
  }
}
npx shadcn@4.23.0 add @xzero-ui-pro/model-arena

Install xzero-foundation once, and import @/components/xzero/foundation.css in your app root. No Xzero token is used by this snapshot. For sample portraits, photos, fonts, or logos, download https://mcp.xzero.co.uk/install-assets.mjs and run node xzero-install-assets.mjs <receiving-project>.

Usage

import { ModelArena, ArenaRunError } from "@/components/xzero/blocks/model-arena/model-arena";

// Stream each model through your own endpoint. Yield text as it arrives.
async function* run(model, prompt, signal) {
  const response = await fetch("/api/arena", { method: "POST", body: JSON.stringify({ model: model.id, prompt }), signal });
  if (!response.ok) throw new ArenaRunError(response.status === 429 ? "Rate limited. Try again in a moment." : "The model did not respond.");
  const reader = response.body.getReader();
  const decoder = new TextDecoder();
  for (;;) {
    const { done, value } = await reader.read();
    if (done) return;
    yield decoder.decode(value, { stream: true });
  }
}

export function Arena() {
  return <ModelArena run={run} onVote={vote => saveVote(vote)} />;
}

API reference

ModelArena

A blind side by side comparison in one card. A single line prompt field with Run and three sample chips sends the same prompt to two models, and two slim panes stream their answers at the same pace inside a fixed height. Each pane header shows "Model A" or "Model B" with a small status (Thinking, Writing, Done), the first pane to finish earns a Finished first marker, and a quiet line under each pane rolls first token time, tokens per second and cost on tabular digits. When both answers land, the vote row offers A is better, Tie, B is better and Both bad. Voting reveals both models: the brand mark and name crossfade in where the letter stood while the label's width follows on a spring with no bounce, and the row becomes a one line result with Next round. A Leaderboard button opens a popover with the top five by win rate, which replays the latest vote as a smooth reorder. A Blind switch reveals names at any time, a pane that fails shows its error and a retry, and below 560px the panes become two tabs that keep both statuses in view.

PropTypeDefaultDescription
contestantsArenaModel[]arenaModelsThe pool each round draws its blind pair from. An ArenaModel has id, name, optional shortName for tabs, provider, an optional mark component with markColor for mono marks, price { input, output } in US dollars per million tokens, and an optional color for its leaderboard bar (a series token such as var(--series-2)). The default pool is Claude Opus 5.5, GPT-6.1 Sol, Gemini 4 Argon, Grok 4.7, Claude Sonnet 5.5, DeepSeek V4.1 Flash and Mistral Large 4; Anthropic prices are list prices and the others are sample values.
promptsArenaPrompt[]arenaPromptsSample prompts shown as chips under the field, each { id, label, text }. The first one prefills the field, a chip fills and runs its prompt, and Next round moves to the next one.
run(model: ArenaModel, prompt: string, signal: AbortSignal) => AsyncIterable<ArenaStreamEvent>–Streams one model's answer. Yield text deltas as strings, or objects { text?, inputTokens?, outputTokens? } to report the provider's usage; without usage, output tokens are estimated at four characters per token. Throw to fail the pane; throw an ArenaRunError with { retryable: false } to hide the retry. Stop when the signal aborts. Without run, the arena streams scripted sample answers at simulated speeds and says so in its live region.
onVote(vote: ArenaVote) => void–Called once per vote with { round, prompt, models (model A, model B), choice ("a" | "b" | "tie" | "bad"), winner, metrics }. Each metrics entry carries ttftMs, tokensPerSecond, totalMs, outputTokens and costUsd as measured from the stream.
defaultBlindbooleantrueHides model names and marks until a vote. The Blind switch flips it at any time.
autoStartbooleantrue without run, false with runRuns the first round once the card scrolls into view.
initialStandingsRecord<string, { wins: number; ties: number; losses: number }>–Starting records per model id, for a leaderboard loaded from your own store. Without it the leaderboard counts this session's votes from zero.
classNamestring–Class on the root element.

Keyboard interactions

KeysAction
TabMoves through Leaderboard, the Blind switch, the prompt field, Run, the sample chips, each answer (a scrollable region), Retry where shown, and the vote buttons or Next round.
EnterIn the prompt field, runs the prompt. While the models are answering, Run becomes Stop.
Command Enter / Ctrl EnterRuns the prompt from the field, Run or a chip.
EscapeStops a run in progress from the prompt row, and closes the leaderboard popover, returning focus to its button.
Arrow left / Arrow right, Home, EndOn the narrow layout's answer tabs, moves between model A and model B.
Arrow up, Page up, HomeInside a focused answer, scrolls back and stops it following the stream; scrolling to the end follows again.
Enter, SpaceOn a vote button, records the vote and moves focus to Next round, which starts the next blind round and returns focus to Stop.

Accessibility

Motion

Responsive behavior

Performance

Notes for AI

Guidance for AI tools

Blocks are complete, self-contained screens with sample data. Replace the sample data and connect the callbacks described above. Follow the declared prop types and do not invent props. Keep keyboard access, reduced motion support, and both light and dark themes intact when adapting it.

Full library index: https://ui.xzero.co.uk/llms.txt

Browse all components