Chromatic Variant Stage
A product display stage where picking a swatch performs three transitions at once — the background bleeds outward radially, the product image cross-dissolves, and the price and model number flip to their new values.
npx shadcn@latest add https://webberui.com/r/chromatic-variant-stage.jsonPlayground
Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.
<ChromaticVariantStage />
Installation
npx shadcn@latest add https://webberui.com/r/chromatic-variant-stage.jsonOr, once registries are configured in components.json, install it as @webberui/chromatic-variant-stage.
Usage
import {
ChromaticVariantStage,
type ChromaticVariant,
} from "@/components/ui/chromatic-variant-stage";
const variants: ChromaticVariant[] = [
{ id: "coral", name: "Coral", color: "#ff6a55", price: "$269", model: "WB-900 / CR" },
{ id: "azure", name: "Azure", color: "#4f8ff0", price: "$289", model: "WB-900 / AZ" },
];
<ChromaticVariantStage title="Webber wireless headphones" variants={variants} defaultValue="coral" />When no image is provided, a default product silhouette is generated automatically in the variant's color; to swap in a photograph or an SVG, just put the node in each variant's image field.
Controlled mode
Pass value and onValueChange to drive the selected variant from the outside:
const [color, setColor] = React.useState("coral");
<ChromaticVariantStage
variants={variants}
value={color}
onValueChange={setColor}
/>Props
| Prop | Type | Default | Description |
|---|---|---|---|
variants | ChromaticVariant[] | — | All selectable variants; their order is the order of the swatches |
value | string | — | Controlled: id of the currently selected variant |
defaultValue | string | First variant | Initial variant id in uncontrolled mode |
onValueChange | (id: string) => void | — | Fires when the selected variant changes |
title | string | — | Title above the stage |
swatchLabel | string | "Choose a color" | Accessible label for the swatch group |
duration | number | 0.7 | Duration of the triple transition (seconds) |
ChromaticVariant
| Field | Type | Description |
|---|---|---|
id | string | Unique identifier (used for both controlled and uncontrolled switching) |
name | string | Variant name: the swatch's aria-label and what gets announced |
color | string | The variant's main color (any CSS color value): the swatch, the radial bleed, and the stage background are all derived from it |
price | string | Displayed price (including the currency symbol, such as $129) |
model | string | Model / style code |
image | React.ReactNode | Product visual; when not provided, a default silhouette generated in the variant color is shown |
How it works
- Triple transition: a single click triggers three things at once — the background bleed, the product image cross-dissolve (
AnimatePresenceoverlaps a fade in and out with a slight scale), and the flip that updates the price and model number (arotateXflip around the X axis, with the price flipping up from the bottom edge and the model down from the top). All three share the samedurationand easing, so they read as three facets of one motion. - Anchoring the radial bleed's origin: clicking a swatch measures that swatch's center relative to the stage in real time and converts it to percentages to use as the radial origin; the color therefore bleeds out from the swatch you pressed, rather than always spreading from the center. Moving the selection with the keyboard uses the corresponding swatch's position as the origin in exactly the same way.
- A handoff with no color jump: the bleed layer and the base color layer use the same radial background function, and the bleed expands with
clip-path: circle()from0%to150%(150%guarantees any origin can cover the whole rectangle). At the moment it is fully open, the bleed layer and the base color are pixel-identical, so when the animation ends the base color is advanced to the new variant and the bleed layer is removed — with no flicker at the handoff.
Accessibility
- The swatch group is a
role="radiogroup"and each swatch arole="radio"marking its selected state witharia-checked; it uses a roving tabindex (only the selected swatch can be reached with Tab). - Keyboard operation: the arrow keys move the selection between swatches and focus follows automatically, while
Home/Endjump to the first and last. - The price block carries
aria-live="polite", so the updated price is announced when the variant changes; the default product silhouette carriesrole="img"and anaria-label. - When the user has "reduce motion" enabled at the system level, the background switches directly with no bleed and the product image and text update instantly, with no transitions played.
Cart Arc
An arc into the bag — clicking add-to-cart sends the product thumbnail flying along a bézier arc toward the cart, where the icon squashes like jelly and the badge number pops as it increments; rapid clicks send several in flight at once.
Bundle Builder
A bundle-building machine — click a product thumbnail and it FLIPs into a slot, the total rolls to its new value, and filling the required number of slots completes the discount ring and unlocks the discount.