WebberUI

Data Duel Split

A page-scale A/B duel split screen — scrolling reveals the metrics round by round while the divider in the middle slides like a tug-of-war rope and the running score at the bottom jumps in sync.

This is a WebberUI Pro component

Free during the launch campaign: sign up or sign in, then hit “Copy install command” in the preview above and it installs straight away — no payment, no credit card. The command below returns 401 while you are signed out.

How to install Pro components →See the plans →

Loading preview…
npx shadcn@latest add "https://webberui.com/r/data-duel-split.json?t=<install token>"

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

300
<DataDuelSplit />

Installation

npx shadcn@latest add "https://webberui.com/r/data-duel-split.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/data-duel-split.

Usage

import { DataDuelSplit } from "@/components/ui/data-duel-split";

<DataDuelSplit
  left={{ name: "Nova" }}
  right={{ name: "Atlas" }}
  rounds={[
    { label: "First paint", left: 420, right: 680, suffix: "ms", win: "lower" },
    { label: "Conversion rate", left: 3.1, right: 4.6, suffix: "%", format: (v) => v.toFixed(1) },
    { label: "Monthly active users", left: 128, right: 96, suffix: "k" },
  ]}
/>

By default the whole window is the scroll container and the stage height is 100vh, so dropping it into a page makes it a scroll narrative on its own. To embed it inside an overflow-y-auto block, pass that block's ref to container and set stageHeight to a fixed pixel value:

const scrollerRef = React.useRef<HTMLDivElement>(null);

<div ref={scrollerRef} className="h-[300px] overflow-y-auto">
  <DataDuelSplit container={scrollerRef} stageHeight="300px" left={/* … */} right={/* … */} rounds={/* … */} />
</div>

Props

DataDuelSplit

PropTypeDefaultDescription
leftDuelTeamConfiguration of the left side
rightDuelTeamConfiguration of the right side
roundsDuelRound[]Round-by-round duel data, revealed in scroll order
stageHeightstring"100vh"Stage height (a CSS length); a fixed pixel value is recommended inside a nested container
containerRefObject<HTMLElement | null>Ref of the nested overflow scroll container; defaults to the window as scroll container
classNamestringClass appended to the outermost container

DuelTeam

FieldTypeDefaultDescription
namestringSide name
accentstringLeft #4f46e5 / right #e11d48Accent color (any CSS color), used for the fill, the score, and the winning value
iconReact.ReactNodeSmall icon or logo shown beside the name

DuelRound

FieldTypeDefaultDescription
labelstringName of the metric being compared in this round
leftnumberValue for the left side
rightnumberValue for the right side
format(value: number) => stringtoLocaleStringValue formatting function
suffixstringSuffix for the value, for example "%", "ms", or "k"
noteReact.ReactNodeSupplementary note for this round
win"higher" | "lower""higher"How the winner is decided: the higher value wins or the lower value wins

How it works

  • Tug-of-war divider: useScroll provides the scroll progress, which interpolates continuously between each round's advantage share, and a layer of useSpring on top gives the divider some momentum, like a rope being pulled from both sides. The advantage share is clamped between 12% and 88%, ensuring the labels on both sides always stay visible.
  • Running score: the score counts only the rounds up to and including the current one, and when the round changes the score springs to give that "jump" feeling. In a round with win: "lower", the lower value takes the win.
  • Round value reveal: each round's metric number springs from 0 to the target value as it enters; integers stay integers, and values with decimals keep their decimals.
  • Nested scrolling: the outermost container's height is stageHeight × the number of rounds, and the stage inside is pinned with sticky top-0. container lets it scroll inside any overflow-y-auto block.

Accessibility

  • When the user has "reduce motion" enabled at the system level, it is presented as static row-by-row comparison bars and a final score, no longer depending on scroll or animation.
  • The visual stage is hidden from assistive technology (aria-hidden), and a separate sr-only full summary list announces both sides' values and the winner round by round.
  • The outermost element marks the whole duel with role="group" plus an aria-label, making it easy for assistive technology to identify what the block is for.

On this page