WebberUI

Balance Scale Compare

A balance-scale comparison layout — check feature chips onto either side and the two columns physically sink and rise with their accumulated weight, while the beam on its fulcrum tilts to an angle that reflects the real gap.

Loading preview…
npx shadcn@latest add https://webberui.com/r/balance-scale-compare.json

Playground

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

14
<BalanceScaleCompare />

Installation

npx shadcn@latest add https://webberui.com/r/balance-scale-compare.json

Or, once registries are configured in components.json, install it as @webberui/balance-scale-compare.

Usage

import {
  BalanceScaleCompare,
  type ScaleFeature,
} from "@/components/ui/balance-scale-compare";

const features: ScaleFeature[] = [
  { id: "price", label: "Price", weight: 3, icon: "💰" },
  { id: "speed", label: "Performance", weight: 2, icon: "⚡" },
  { id: "support", label: "Support", weight: 2, icon: "🎧" },
];

<BalanceScaleCompare
  features={features}
  leftLabel="Build it yourself"
  rightLabel="WebberUI"
  defaultAssignments={{ price: "left", speed: "right" }}
/>;

In controlled mode, pass assignments and keep the state in sync with onAssignmentsChange:

const [assignments, setAssignments] = React.useState<
  Record<string, "left" | "right">
>({});

<BalanceScaleCompare
  features={features}
  assignments={assignments}
  onAssignmentsChange={setAssignments}
/>;

Props

PropTypeDefaultDescription
featuresScaleFeature[]— (required)List of feature chips that can be assigned to either side
leftLabelstring"Plan A"Heading of the left column
rightLabelstring"Plan B"Heading of the right column
assignmentsRecord<string, "left" | "right">Controlled assignment map; chips not in the map are unassigned
defaultAssignmentsRecord<string, "left" | "right">{}Uncontrolled initial assignment map
onAssignmentsChange(next: Record<string, "left" | "right">) => voidFires when an assignment changes, carrying the complete new assignment map
maxTiltnumber14Maximum tilt angle (degrees), the limit reached when all the weight is on one side
unitstring"pts"Unit label for the weights
classNamestringClass appended to the outermost container

ScaleFeature

FieldTypeDefaultDescription
idstring— (required)Unique identifier key
labelstring— (required)Display name
weightnumber1Weight (a positive number); the totals determine how the scale tilts
iconReact.ReactNodeOptional icon (emoji or node), shown before the chip

How it works

  • Physical tilt: the weight difference between the two sides ÷ the total weight is normalized to -1..1 and multiplied by maxTilt to get the angle, so the scale settles at an angle that reflects the real gap; it reaches the limit when everything is on one side, and returns level when the two sides are equal
  • Pan sinking: when the beam rotates θ degrees, the vertical offset of each end is sin(θ) × half the beam length, and each pan sinks or rises along with its end — the heavier side goes down, the lighter side comes up
  • Layer separation: the beam and the fulcrum pointer belong to the same rotation group that rotates around the fulcrum; the horizontal placement of the pans is handled by an outer element while the vertical sinking is handed to an inner motion element, so the two transforms do not interfere with each other
  • Swing to rest: the beam and the pans share a spring with a slight overshoot, so after an assignment changes it wobbles a few times before settling, just like a real balance scale
  • Chips in and out: a chip checked onto a side springs into that pan, AnimatePresence shrinks and fades out chips that leave, and the remaining chips fill the gap with a layout animation

Accessibility

  • Each side button is a native button with an aria-label (containing the chip name and the target side's heading) and aria-pressed marking the current selection, and it can be operated with the keyboard
  • The scale graphic is hidden from assistive technology with aria-hidden; a separate role="status" + aria-live="polite" text region announces which side is currently heavier and by how much
  • When the user has "reduce motion" enabled at the system level, the beam and pans switch instantly and chips do not scale as they come and go; functionality is entirely unaffected

On this page