WebberUI

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

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/anchored-buy-rail.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.

3280
4180
96
<AnchoredBuyRail />

Installation

npx shadcn@latest add "https://webberui.com/r/anchored-buy-rail.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
titlestringProduct name
pricenumberPrice, formatted together with currency
compareAtPricenumberOriginal price (shown struck through)
currencystring"$"Currency symbol
thumbnailReactNodeProduct thumbnail, which FLIPs between the panel and the capsule
subtitlestringSubtitle; shown in the capsule when there are no variants
variantsBuyVariantGroup[]Single-select variant groups (size, color, …)
valueBuySelectionControlled selection value
defaultValueBuySelectionFirst available option in each groupUncontrolled initial selection value
onValueChange(v: BuySelection) => voidCallback when the selection changes
onAddToCart(v: BuySelection) => voidCallback when the CTA is pressed
ctaLabelstring"Add to cart"CTA copy
noteReactNodeFree shipping messageTrust message at the bottom of the panel
childrenReactNodeFreely scrolling content in the left column
footerReactNodeFooter 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.

On this page