affectUI › Components › Data display

Experiment Card

An A/B test in two lines that quietly unfolds on hover – variants linked to a tiny chart, a metric switch with rolling numbers, and a winner you pick with a click.

Part of affectUI Pro. Install: npx shadcn@latest add https://www.affectui.com/r/experiment-card.json · Import from @/components/ui/experiment-card

When to use

  • A/B tests, prompt or model comparisons, pricing experiments – anything with a few variants measured the same way
  • Lists of running experiments where the result should be one hover away
  • When people should not only read the result but act on it: pick the winner and ship it

When not to use

  • More than about five variants – the head chart and the rows get crowded; use a table
  • Results over time – a line chart shows when a variant pulled ahead; this card shows where things stand
  • A single number without variants – use KPI Chart or Compact Breakdown

Usage

import { ExperimentCard } from "@/components/ui/experiment-card";

export function SignupCopyTest() {
  return (
    <ExperimentCard
      title="Signup button copy"
      status={{ label: "Running", tone: "green" }}
      note="Day 9 of 14"
      metrics={[
        { id: "conversion", label: "Conversion", format: (n) => `${n.toFixed(1)}%` },
        { id: "revenue", label: "Revenue", format: (n) => `$${n.toFixed(2)}` },
      ]}
      variants={[
        { id: "a", key: "A", name: "Start free trial", control: true, visitors: 4790, values: { conversion: 4.2, revenue: 18.4 } },
        { id: "b", key: "B", name: "Try it free", visitors: 4812, values: { conversion: 4.7, revenue: 19.1 }, confidence: { conversion: 96, revenue: 81 } },
      ]}
      action={{ label: (v) => `Ship ${v.key}`, onClick: (v) => ship(v.id) }}
    />
  );
}

Props

NameTypeDefaultDescription
titlestringName of the experiment
variantsExperimentVariant[]{ id, key, name, values, control?, visitors?, confidence?, color? } – values and confidence hold one number per metric id; key is the short letter in the badge
metricsExperimentMetric[]{ id, label, format?, lowerIsBetter? } – the switch above the rows; no switch with a single metric
metricstringControlled metric id
defaultMetricstringfirst metricStarting metric when uncontrolled
onMetricChange(id: string) => voidCalled with the new metric
winnerstring | nullControlled id of the picked variant
defaultWinnerstring | nullnullPicked variant when uncontrolled
onWinnerChange(id: string | null) => voidCalled when a variant is picked, or null when the pick is taken back
status{ label: string; tone?: ExperimentTone }The tag under the title – tone is red, amber, green, blue, violet or gray
notestringShort text next to the status, e.g. "Day 9 of 14"
action{ label: string | (variant) => string; onClick: (variant) => void }The button that appears on the tray once a winner is picked
hintReactNode"Pick a winner"The line on the tray while nothing is hovered or picked
trigger"hover" | "click""hover"Hover opens under the mouse; taps and the keyboard always toggle
openbooleanControlled open state
defaultOpenbooleanfalseStarts open when uncontrolled
onOpenChange(open: boolean) => voidCalled when the card opens or closes
classNamestringExtra class on the root

Keyboard

  • Enter / Space: On the card: opens or closes it
  • ← / →: In the metric switch: previous or next metric (wraps around)
  • Tab: Through the metric switch, the variants and the action
  • Space: On a variant: picks it as the winner, or takes the pick back
  • Esc: Closes the card and returns focus to it

Accessibility

  • The head is one button with aria-expanded; its name carries the status and who is ahead
  • The metric switch and the variants are radio groups; every variant's name includes its value and its change against the baseline
  • The small chart in the head is decorative – everything it shows is in the rows
  • While the card is closed, everything inside is inert; the line on the tray is announced politely

Responsive

  • Fills its container up to 340px; below 300px the note next to the status goes and the chips tighten
  • Touch screens: a tap opens, a tap outside closes, rows and the metric switch are taller
  • Dark mode built in: switches when an ancestor (usually <html>) has the class dark or data-theme="dark"

Requirements

  • React 19

Related

  • Project Card – A two-line project summary that quietly unfolds on hover – a progress ring with one segment per subtask, a people filter and a status picker, all linked by hover
  • Compact Breakdown – A total split into its parts in one small card – numbers roll, the bar glides and the ranked rows trade places when the period changes
  • KPI Chart – Metric card with a multi-line chart and a tooltip that follows the pointer