Hero ux en

Sticky Add-to-Cart on Mobile PDPs: the Buy-Bar Pattern

What the sticky buy-bar is and why it holds the buying moment

The sticky add-to-cart bar, or buy-bar for short, is the bar pinned to the bottom of the mobile product detail page (PDP) that keeps price, variant status, and the primary purchase button visible at all times. No matter how far the user scrolls, the buying moment stays one thumb-tap away.

That is the short answer. The longer one starts with the problem the buy-bar solves. On a phone, the PDP is long. Gallery, title, price, variants, description, shipping info, reviews, cross-sells: the original add-to-cart button sits near the top and disappears from view after a few swipes. A shopper who decides to buy after reading the reviews has to scroll back up. Every one of those return trips is a chance to abandon.

The buy-bar removes that return trip. It is not the same as the mini-cart drawer that pops up after you add an item, and it is not the same as the image gallery. It is its own component with its own logic: it appears when the original purchase button scrolls out of the viewport, it mirrors the variant state of the page, and it triggers the same add-to-cart action. Sounds simple. That is exactly why it is so often built wrong: triggered too late, with tap targets that are too small, with a lagging variant state, or with a layout shift that wrecks the Core Web Vitals score.

This post breaks the buy-bar down as a pattern in its own right: its anatomy, the trigger moment, the accessibility traps, the variant and out-of-stock states, and the performance side. Plus the reason it is a frontend-layer decision, not a backend feature.

Component anatomy: price, CTA, variant status

A buy-bar has three mandatory elements and a few optional ones. The mandatory ones are price, primary CTA, and variant status. Leave any of them out and you have not built a buy-bar, you have built a floating button that has lost its context.

  • Element | Required? | Function | Common mistake
  • Price (incl. strikethrough where relevant) | Yes | Confirms what is being bought, no scroll-back | Price does not update on variant change
  • Primary CTA (Add to cart) | Yes | The actual buying moment | Tap target under 44px, unclear label
  • Variant status (e.g. Size M, Blue) | Yes | Shows which variant will be added | Missing, user buys the wrong variant
  • Product thumbnail | Optional | Visual anchor on a long page | Crowds out space for the tap target
  • Quantity stepper | Optional | Direct quantity change | Makes the bar too tight on mobile
  • Wishlist / Save | Optional | Secondary action | Competes visually with the CTA

The order in tight space matters. Context on the left (price and variant status, optionally with a thumbnail), action on the right (the CTA as the clearly dominant element). The CTA is the only part of the bar allowed to claim visual weight: fully filled, wide enough, with a clear label. Secondary actions like Save stay quiet, otherwise a second focal point appears and pulls attention away from the purchase.

What matters most is the live link between page state and bar state: if the user picks Size L and Green on the page, the buy-bar has to mirror exactly that before they press the button. A bar still showing the default variant while the user has long since chosen another leads to wrong purchases and returns.

When the bar triggers: the scroll moment

The trigger is the trickiest part. The buy-bar should appear as soon as the original add-to-cart button leaves the visible area, and disappear again when it comes back into the viewport. That avoids redundancy: as long as the real button is visible, there is no need for a second bar.

The clean implementation uses an Intersection Observer on the original button, not a scroll event listener with a pixel threshold. The Intersection Observer fires only on an actual visibility change, runs outside the main-thread scroll path, and avoids the janky stutter that scroll listeners cause on long pages. A hardcoded pixel value (show the bar after 600px of scroll) breaks the moment the page gets longer or shorter on a different device.

Two detail rules make the difference:

  • Soft transition, not a hard jump. The bar should fade or slide in over a short transition (roughly 150 to 200 milliseconds) so the change does not read as a visual shock. But the transition must not shift the layout, more on that below.
  • No hysteresis gap. If the bar appears and disappears at exactly the same threshold, it flickers on the smallest scroll near the boundary. A small buffer between appearing and disappearing prevents the flicker.

Accessibility traps: focus order, tap target, contrast

The buy-bar is a classic accessibility trap because it sticks to the bottom visually but can sit anywhere in the DOM. That breaks expectations for keyboard and screen reader users. Three points are non-negotiable.

Focus order. When the buy-bar appears visually at the bottom of the page but is mounted early in the DOM, keyboard focus jumps to an unexpected place. The bar has to be focusable in a logical order, and its appearance must not move focus unprompted. Forcing focus into the bar when it appears yanks keyboard users out of their reading flow. Rule: the bar appears, focus stays where it was, until the user navigates there themselves.

Tap target at least 44 by 44 pixels. The WCAG recommendation for touch targets is 44 by 44 CSS pixels (success criterion 2.5.5, Target Size). The CTA in the buy-bar is the single most important tap point on the whole page, so there is no reason to go below it. Watch out for icon-only buttons such as a heart for the wishlist: the visible icon is often 24px, but the tap target still has to cover the full 44px area, otherwise thumbs miss it.

Contrast. The CTA text has to meet the WCAG contrast ratio (4.5:1 for normal text). A semi-transparent bar over shifting page content is tempting but risky: when light content scrolls under a light bar, contrast collapses. A solid background with a fine top divider is safer.

On top of that, the bar should be marked up cleanly for screen readers so it is clear this is a recurring purchase action and not a second, separate button.

Variant and out-of-stock states

The buy-bar has to mirror every state of the page, including the awkward ones. A bar that only ever offers Add to cart ignores the reality of variants and stock.

  • State | Buy-bar shows | CTA behavior
  • Variant selected, in stock | Price + selected variant | Add to cart, active
  • No variant selected | From-price, prompt to select | CTA scrolls to variant selection
  • Selected variant out of stock | Price + out-of-stock note | CTA becomes Notify me or is disabled
  • Fully out of stock | Out-of-stock status | Notify-me CTA instead of cart
  • Available to pre-order | Price + delivery date | CTA becomes Pre-order

Two traps wait here. First: a disabled button with no explanation. If the CTA is greyed out and not clickable but the user does not know why, frustration follows. Better is a CTA that responds to the missing action, for example scrolling up to the variant selection or naming the out-of-stock reason. Second: the state-sync lag. When the user switches to an out-of-stock variant, the bar has to flip immediately. A bar still showing Add to cart while the variant is long out of stock produces a failing action and exactly the distrust the buy-bar is meant to prevent.

Performance: no layout shift, no CLS

This is where the UX question meets Core Web Vitals. A poorly built buy-bar is one of the most common triggers of bad Cumulative Layout Shift (CLS) on mobile PDPs.

The problem: if the bar is inserted into the layout flow on trigger and pushes the content above it upward, the page jumps. Right at the moment the user might be tapping. That is not just a metric problem, it is a misclick risk.

The clean implementation:

  • Position fixed, outside the document flow. The buy-bar sits as a fixed overlay above the content, not in the flow. Its appearance moves nothing.
  • Reserve space from the start. The bottom of the page needs padding the height of the bar so the last content is not permanently hidden. That padding is set once, not on trigger.
  • Animate transform, not layout properties. Showing and hiding via transform: translateY and opacity runs on the compositor and triggers no re-layout. Animating via height or top, by contrast, forces layout work and can cause CLS.

The public thresholds for orientation: LCP under 2.5 seconds, INP under 200 milliseconds, CLS under 0.1. The buy-bar touches CLS and INP above all: CLS, because its appearance must move nothing, and INP, because the tap on the CTA has to respond without noticeable delay.

Why the buy-bar is a frontend-layer decision

The most common mistake: the buy-bar is treated as a detail of the theme template that only developers touch. The backend supplies price, stock, and variants. But when the bar triggers, how big the tap target is, what the out-of-stock state looks like, how it animates: those are all frontend decisions.

In a classic monolithic storefront, these rules are buried deep in the template. A change to the trigger threshold or the out-of-stock behavior means a ticket, a sprint, a deployment. A/B tests on whether the bar converts better with or without a thumbnail are barely feasible, because every variant is code work.

This is exactly where a Frontend Management Platform (FMP) comes in. The frontend layer is standalone, backend-agnostic, and component-based. The buy-bar is a configurable component: trigger behavior, tap-target size, state logic, and animation are exposed and editable in the editor, without touching the backend behind it (Shopify, Shopware, commercetools, or any of the 50+ supported backends). That is the heart of the Pillar 2 idea: a storefront layer that works independently of the backend release cycle.

And the performance side comes along: through the Performance and Core Web Vitals layer, CLS and INP are monitored in the field, so a buy-bar that suddenly causes layout shift surfaces immediately instead of only in the next audit.

Teams that want to lift the conversion lever of mobile storefronts systematically will find the right entry point in the Growth-Kit for B2C: concrete patterns for mobile-first storefront optimization, from the buy-bar to checkout.

FAQ

When should the buy-bar appear? As soon as the original add-to-cart button scrolls out of the viewport, controlled by an Intersection Observer on that button, not by a fixed pixel threshold. When the real button leaves view, the bar appears; when it returns, the bar disappears.

How large does the CTA in the buy-bar have to be? At least 44 by 44 CSS pixels as a tap target, per WCAG success criterion 2.5.5. For icon buttons, the invisible tap area has to cover the full 44px even if the icon is smaller.

Does a sticky buy-bar hurt Core Web Vitals? Only when built wrong. With position: fixed, padding reserved from the start, and animations via transform instead of height or top, it causes no layout shift and stays under the CLS target of 0.1.

What sets the buy-bar apart from the mini-cart? The buy-bar keeps the buying moment available before the click (price, variant, CTA). The mini-cart drawer appears after you add an item and shows the cart contents. Two different components for two different moments, see our mini-cart UX patterns.

Does every PDP need a sticky buy-bar? On mobile almost always, because the page is long there and the original button drops out of view fast. On desktop the benefit is smaller, because more fits above the fold. Mobile-first means: build for the phone, scale back on desktop where needed.

Next steps

The sticky buy-bar is not a cosmetic detail, it is the component that holds the buying moment on mobile PDPs. The points that count:

  • Three mandatory elements: price, primary CTA, variant status. Live in sync with the page.
  • Trigger via Intersection Observer, not hardcoded pixel values.
  • Tap target at least 44px, focus order clean, contrast checked.
  • Cover every state: in stock, no variant selected, out of stock, pre-order.
  • No CLS: position fixed, reserve padding, animate via transform.

And the bigger point: all of this is frontend-layer decisions. They belong in a render layer that makes them fast, testable, and shippable independently of the backend.

Further reading

Read more

Frontend insights for you

Book a demo mobile
Contact

Your next level starts here.

No complex setups, no performance slowdowns. Regain full control over your digital customer experience.