WebberUI

Versus Tray

Check off products and a comparison tray slides out from the bottom; the thumbnails FLIP into a full-width spec table that scans row by row and marks the winner of each one.

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/versus-tray.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.

4
4
260
<VersusTray />

Installation

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

Or, once registries are configured in components.json, install it as @webberui/versus-tray.

Usage

import {
  VersusTray,
  type VersusProduct,
  type VersusSpec,
} from "@/components/ui/versus-tray";

const specs: VersusSpec[] = [
  { key: "price", label: "Price", better: "lower", unit: " NTD" },
  { key: "battery", label: "Battery life", better: "higher", unit: " hours" },
  { key: "os", label: "OS" },
];

const products: VersusProduct[] = [
  {
    id: "a",
    name: "Aero X1",
    subtitle: "Flagship ultralight",
    specs: { price: 27900, battery: 22, os: "Android 15" },
  },
  {
    id: "b",
    name: "Nova Pro",
    subtitle: "Camera flagship",
    specs: { price: 31900, battery: 20, os: "Android 15" },
  },
];

<VersusTray products={products} specs={specs} maxSelect={4} />;

Props

VersusTray

PropTypeDefaultDescription
productsVersusProduct[]List of products
specsVersusSpec[]Spec row definitions, used in order as each row of the table
selectedIdsstring[]Controlled: ids of the checked products
defaultSelectedIdsstring[][]Initial checked set in uncontrolled mode
onSelectedChange(ids: string[]) => voidFires when the checked set changes
onCompareChange(comparing: boolean) => voidFires on entering or leaving the comparison view
maxSelectnumber4Maximum number that can be compared at once
columnsnumber3Number of columns in the product grid
scanIntervalnumber260Interval between rows of the row-by-row scan (milliseconds)
compareLabelstring"Compare"Text on the compare button
backLabelstring"Back to picking"Text on the back-to-picking button
hideSameLabelstring"Differences only"Text for the "Differences only" toggle

VersusSpec

FieldTypeDescription
keystringSpec identifier key, matching the index in product.specs
labelstringName shown at the head of the row
better"higher" | "lower" | "none"Which direction wins; none (the default) does not compare
unitstringDisplay unit, appended after the value
format(value) => ReactNodeCustom value presentation, overriding the default format

VersusProduct

FieldTypeDescription
idstringUnique identifier key
namestringProduct name
subtitlestringSubtitle (price, model number, and so on)
thumbnailReactNodeThumbnail content; when not provided, the first letter of the name is used as a placeholder
specsRecord<string, string | number | boolean | null>Table of spec values

How it works

  • Collection tray: once products are checked, the comparison tray slides out from the bottom of the container on a spring; the chips animate as they enter, exit, and reorder, and can be removed one at a time.
  • FLIP fly-in: pressing "Compare" makes the thumbnails in the collection tray fly in with a shared layoutId and scale up into the spec table's column headers, then fly back to their original places on return.
  • Row-by-row scan: after entering comparison, rows light up one at a time according to scanInterval, with a highlight sweeping across the row as it is scanned.
  • Winner marking: the best value in each row is found according to the better direction (numeric values, ties supported) and marked with a pulsing green background and a check mark; once the scan finishes, the product with the most winning rows — if it is unique — gets a lead badge on its column header.
  • Differences only: one click hides every row where all products have the same value, and the remaining rows FLIP into place.
  • Boolean values are presented as "Yes / No" by default, and format can customize the display of any type.

Accessibility

  • Product cards are role="checkbox" with aria-checked, focusable with the keyboard and checkable with Enter / Space; once maxSelect is reached, unselected cards become disabled.
  • The spec table uses the semantics role="table" / row / columnheader / rowheader / cell, winning cells carry a "Best" label, and the lead badge carries hidden "Leading" text.
  • The collection tray is a role="region" and the remove buttons carry matching aria-labels; the "Differences only" toggle carries aria-pressed.
  • When the user has "reduce motion" enabled at the system level, the slide-out, FLIP flight, and scan are all disabled and the spec table shows the complete result with its winner marks right away.

On this page