Commerce & Checkout

Anchored Buy Rail

An anchored buy rail — a long-scrolling left column beside a sticky buy panel that FLIPs down into a mini checkout capsule as you scroll, then expands again as the reviews section comes near.

This is a WebberUI Pro component

The install command below needs a licence to run; without one it returns 401.

How to install Pro components →See the plans →

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add "https://webberui.com/r/anchored-buy-rail.en.json?t=<install token>"

New project or installation trouble? Start here →

Playground

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

3280
4180
96
Active edited usageDefault props
<AnchoredBuyRail/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add "https://webberui.com/r/anchored-buy-rail.en.json?t=<install token>"

Or, once registries are configured in components.json, install it as @webberui/anchored-buy-rail.

Usage

import { AnchoredBuyRail } from "@/components/ui/anchored-buy-rail";

<AnchoredBuyRail
  title="Terra organic cotton sweatshirt"
  price={1280}
  compareAtPrice={1680}
  currency="NT$"
  variants={[
    {
      id: "size",
      label: "Size",
      options: [
        { value: "s", label: "S" },
        { value: "m", label: "M" },
        { value: "l", label: "L" },
      ],
    },
  ]}
  onAddToCart={(selection) => console.log(selection)}
  footer={<Reviews />}
>
  <ProductStory />
</AnchoredBuyRail>

Put freely scrolling children (media and description blocks) in the left column; the buy panel on the right is driven by props and follows along stickily on its own. If the component is placed inside a nested overflow scroll container, pass that container's ref to container.

Props

PropTypeDefaultDescription
titlestring—Product name
pricenumber—Price, formatted together with currency
compareAtPricenumber—Original price (shown struck through)
currencystring"$"Currency symbol
thumbnailReactNode—Product thumbnail, which FLIPs between the panel and the capsule
subtitlestring—Subtitle; shown in the capsule when there are no variants
variantsBuyVariantGroup[]—Single-select variant groups (size, color, …)
valueBuySelection—Controlled selection value
defaultValueBuySelectionFirst available option in each groupUncontrolled initial selection value
onValueChange(v: BuySelection) => void—Callback when the selection changes
onAddToCart(v: BuySelection) => void—Callback when the CTA is pressed
ctaLabelstring"Add to cart"CTA copy
noteReactNodeFree shipping messageTrust message at the bottom of the panel
childrenReactNode—Freely scrolling content in the left column
footerReactNode—Footer block (the reviews section); the panel expands as it comes near
containerRefObject<HTMLElement | null>WindowRef of the nested scroll container
stickyTopnumber16Sticky pinning position (px)
condenseAfternumber96How many px past the top it must scroll before condensing into the capsule
expandMarginnumber160How many px from the footer it expands early

BuyVariantGroup / BuyOption

interface BuyVariantGroup {
  id: string;
  label: string;
  options: { value: string; label: string; disabled?: boolean }[];
}
type BuySelection = Record<string, string>; // { [group id]: option value }

How it works

  • Sticky + FLIP: the right column follows the scroll with position: sticky; the price, thumbnail, and CTA carry the same layoutId, so as it condenses and expands, Motion's shared layout animation (FLIP) tweens continuously between the full panel and the capsule.
  • When it condenses: two sentinels plus an IntersectionObserver decide it — the top sentinel condenses the panel once it has scrolled condenseAfter past the pinning line, and the panel expands when the top edge of footer enters the viewport (including the expandMargin lead). That means it works inside the window as well as inside any nested scroll container.
  • Controlled and uncontrolled: providing value makes it controlled; otherwise it maintains internal state from defaultValue (or the first available option in each group).

Accessibility

  • Variant groups are role="radiogroup" and options are role="radio" with aria-checked; a roving tabindex supports moving the selection within a group with the arrow keys, and disabled options are skipped automatically.
  • The CTA is a native <button> with a visible focus ring on focus.
  • The buy panel is a role="region" with an aria-label.
  • When the user has "reduce motion" enabled, the panel stays permanently in its full state with the condense and FLIP animations disabled, and every buy control remains usable throughout.