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.
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.
<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
| Prop | Type | Default | Description |
|---|---|---|---|
products | VersusProduct[] | — | List of products |
specs | VersusSpec[] | — | Spec row definitions, used in order as each row of the table |
selectedIds | string[] | — | Controlled: ids of the checked products |
defaultSelectedIds | string[] | [] | Initial checked set in uncontrolled mode |
onSelectedChange | (ids: string[]) => void | — | Fires when the checked set changes |
onCompareChange | (comparing: boolean) => void | — | Fires on entering or leaving the comparison view |
maxSelect | number | 4 | Maximum number that can be compared at once |
columns | number | 3 | Number of columns in the product grid |
scanInterval | number | 260 | Interval between rows of the row-by-row scan (milliseconds) |
compareLabel | string | "Compare" | Text on the compare button |
backLabel | string | "Back to picking" | Text on the back-to-picking button |
hideSameLabel | string | "Differences only" | Text for the "Differences only" toggle |
VersusSpec
| Field | Type | Description |
|---|---|---|
key | string | Spec identifier key, matching the index in product.specs |
label | string | Name shown at the head of the row |
better | "higher" | "lower" | "none" | Which direction wins; none (the default) does not compare |
unit | string | Display unit, appended after the value |
format | (value) => ReactNode | Custom value presentation, overriding the default format |
VersusProduct
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier key |
name | string | Product name |
subtitle | string | Subtitle (price, model number, and so on) |
thumbnail | ReactNode | Thumbnail content; when not provided, the first letter of the name is used as a placeholder |
specs | Record<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
layoutIdand 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
betterdirection (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
formatcan customize the display of any type.
Accessibility
- Product cards are
role="checkbox"witharia-checked, focusable with the keyboard and checkable with Enter / Space; oncemaxSelectis reached, unselected cards becomedisabled. - 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 matchingaria-labels; the "Differences only" toggle carriesaria-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.
Receipt Cart
A shopping cart built on the thermal-receipt metaphor — line items print in one row at a time, the perforated paper edge extends, the subtotal and free-shipping progress at the tail roll to their new values, and removing a row strikes it through and tears it out.
Parcel Journey
A post-purchase tracking timeline — the parcel advances along a vertical dashed path as the order status changes, completed nodes check off and light up in order, the current node pulses like a breath, and the estimated delivery date is presented as a flipping calendar page.