Skip to content
WingoUI
ComponentsTemplatesDocsPricingAI agentsBlogChangelogContact
Components
Buttons & Actions
ButtonButton GroupCopy ButtonDelete ButtonTheme ToggleToggleToggle Group
Inputs
Avatar UploadCalendarCheckboxColor PickerComboboxCountry SelectCron PickerCurrency InputDate PickerDate Range PickerDate Time PickerDuration PickerEmail InputEmoji PickerFile UploadFilter BuilderInputInput GroupListboxMasked InputMention InputMonth PickerMulti SelectNumber InputOTP InputPassword InputPhone InputRadio GroupRatingSearch InputSegmented ControlSelectSignature PadSliderSwitchTag InputTextareaTime PickerTimezone SelectTree Select
Forms
Address InputBooking WidgetCSV ImportFieldForm WizardInline EditInvoice line itemsRich Text EditorSchema Builder
Navigation
Back To TopBounce SidebarBreadcrumbCommand MenuGlobal SearchGooey NavHook SidebarNav MenuNav TabsPaginationProximity SidebarScroll ProgressSidebarStepsTable Of ContentsTabsTopbarUser MenuWorkspace Switcher
Overlays
Alert DialogContext MenuDialogDrawerDropdown MenuFamily DrawerHover CardPopoverShare DialogSheetShortcuts DialogTooltipTour
Feedback
AlertBannerCookie ConsentEmoji ReactionEmpty StateNotification BellNotification CenterProgressRoute ProgressSetup ChecklistSkeletonSpinnerToast
Data Display
Approval FlowAvatarBadgeCalendar ViewCode BlockComment ThreadContact cardCountdownDescription ListDiff ViewerFile ListFile managerFolderGanttJSON ViewerKbdLog ViewerMarkdownPresenceSchedulerTagTimelineTree View
Tables & Lists
Audit LogData TableDeal PipelineEditable GridKanbanListMembers tablePermissions MatrixTableTask ListVirtual List
Charts & Stats
Animated CounterArea chartBar chartDonut chartFunnel chartGaugeGitHub ActivityHeatmap calendarLeaderboardLine chartRadar chartRadial barSparklineStat CardTrackerTrend badge
Layout
AccordionApp ShellCardCollapsibleDetail LayoutEntity HeaderPage HeaderScroll AreaSeparatorSettings LayoutSplit ViewToolbar
Media
Audio PlayerCarouselCSV ViewerDOCX EditorDOCX ViewerExcel ViewerFile SystemImage compareImage CropperImage GalleryImage LightboxLocation MapPDF ViewerPowerPoint ViewerQR CodeStep PlayerVideo Player
AI Kit
Agent Status CardChat MessageChat ThreadChat WidgetConversation ListFluid OrbGrid RevealMatrix OrbModel PickerPrompt InputReasoning IndicatorRule BuilderSuggestion ChipsTool Call TimelineUsage MeterVoice InputWorkflow Canvas
Text & Effects
Background PatternGravity LettersMarqueeTerminalText AnimateText Rotate
Mobile
Action SheetBottom Tab BarFABHaptic ButtonHero HeaderInstall PromptLarge Title HeaderNotification BannerOnboarding CarouselPicker WheelPull To RefreshSlide To ConfirmSticky CTA BarSwipe DeckSwipeable Tabs
Commerce
Cart drawerCategory pickerCheckout stepsCoupon inputFavorite buttonFilter panelListing cardListing gridOrder trackerPricePrice rangePricing tableProduct galleryQuantity stepperReviewsSeller card
Marketing Sections
App Download SectionBlog Cards SectionCTA SectionFAQ SectionFeature GridFeature ShowcaseHero SectionIntegrations SectionLogo CloudMarketing FooterMarketing HeaderMarketing SectionNewsletter SectionPricing SectionStats SectionSteps SectionTeam SectionTestimonials SectionTimeline SectionVideo Section
Blocks
Admin Dashboard PageAdmin ShellAI Chat PageAPI Keys PageAutomation Builder PageCheckout PageCRM Contact PageCRM Pipeline PageError PagesInbox PageInvoice PageKPI DashboardListing Detail PageLogin PageMarketplace Home PageMarketplace Search PageMarketplace ShellMobile App HomeOnboarding FlowOrder History PagePost Listing WizardPricing PageProfile PageRegister PageSettings PageUsers Admin Page
Templates
About PageAcademy PageAccessibility PageAffiliate PageAgency PageAI Product PageAppointment Booking PageBlog PageBlog Post PageBug Bounty PageCareers PageChangelog PageCommunity PageCompare PageContact PageCustomer Story PageCustomers PageDemo Booking PageDevelopers PageDocs Home PageDownload PageEnterprise PageEvents PageExperts PageFAQ PageFeature PageGlossary PageHelp Article PageHelp Center PageHome Services PageImpact PageIntegration Detail PageIntegrations PageJob Detail PageLanding PageLaunch PageLegal PageManifesto PagePortfolio PagePress PageProduct Tour PagePromo PageReal Estate PageReferral PageResearch Report PageRestaurant PageRoadmap PageROI Calculator PageSecurity PageSolutions PageStartups PageStatus PageStorefront PageSwitch PageTemplate Gallery PageTravel PageWaitlist PageYear in Review Page
Hooks & Utilities
AI ChatChart kitCommerceDate utilsFile exportFile typesFiltersForm validationHapticsInvoicingMotion presetsNavigationNumber formatRelative timeRomanian locationsSpreadsheetText matchTonesUIProvideruseClipboarduseConsentuseControllableStateuseCooldownuseDebounceuseKeyboardInsetuseLocaleuseLongPressuseMediaControlsuseMediaQueryuseOverflowItemsusePersistedStateuseScrollEdgesuseScrollStateuseSelectionuseShortcutuseStickToBottom
GalleryTheme

Mobile

Sticky CTA Bar

Mobile

Sticky CTA Bar React component for Tailwind CSS v4

Slide To ConfirmSwipe Deck
Live props
Content
priceNone
currency
locale
priceSuffix
compareAtPriceNone
caption
actionLabel
Appearance
variant
tone
color
size
radius
State
loading
disabled
Behavior
showOn
threshold200px
hideOnScroll
keyboard
Layout
position
hideFrom
safeArea
Code
import { StickyCtaBar } from "@/components/ui/sticky-cta-bar"

<StickyCtaBar />
  1. Home
  2. Components
  3. Mobile
  4. Sticky CTA Bar
Pro

Sticky CTA Bar React component for Tailwind CSS v4

v1.0.0·Added in 1.0.0

A React sticky CTA bar for product, listing and checkout pages on phones: the price on one side, the main action on the other.

The bar sits at the bottom edge, above a bottom tab bar and the home indicator, with the price (an old price struck through, a muted caption) on the left and the main button on the right, plus an optional icon button such as a heart. With showOn="anchor" it stays out of the way while the page's own buy button is on screen and glides up on a smooth spring once you scroll past it (or while you have not reached it yet), then drops away quickly when the button comes back; showOn="scroll" does the same after a scroll distance. With hideOnScroll it follows your finger down and out while you scroll down and comes back as soon as you scroll up. Tap the button: a promise from onAction shows its spinner without changing the width. When the price changes the new amount fades in. When the on-screen keyboard opens (a coupon field) the bar slides away by default, or rides on top of the keyboard with keyboard="lift". Everything is a normal button, reached with Tab and pressed with Enter or Space; while hidden the bar is inert so Tab skips it, and while keyboard focus is inside it never hides. Under reduced motion it fades instead of sliding.

Sticky CTA Bar is a Pro React component in the React mobile components category, written in TypeScript for React 19 and Tailwind CSS v4. It installs as source with npx wingo-ui@latest add sticky-cta-bar, together with Button, Number format, useLocale, useScrollState, useKeyboardInset and 3 more and the npm package motion.

Add to a project

Requirements: React 19 and Tailwind CSS v4, in Next.js (App Router or Pages), Vite or React Router. Installation guide · Next.js setup · Vite setup

npx wingo-ui@latest add sticky-cta-bar

Sticky CTA Bar is part of Pro, with 298 more Pro items: $8 a month or $150 once. 14-day refund before you download any Pro item. See plans

Usage

Import StickyCtaBar from @/components/ui/sticky-cta-bar.

import { Heart } from "lucide-react"
import { Button } from "@/components/ui/button"
import { StickyCtaBar } from "@/components/ui/sticky-cta-bar"

export function ListingBuyBar({ onBuy }: { onBuy: () => Promise<void> }) {
  return (
    <StickyCtaBar
      price={349}
      compareAtPrice={449}
      caption="Tax included · Free shipping"
      locale="en-US"
      actionLabel="Add to cart"
      tone="brand"
      onAction={onBuy}
      secondaryAction={
        <Button variant="soft" size="icon" radius="full" aria-label="Save">
          <Heart />
        </Button>
      }
      labels={{ region: "Purchase", compare: "Old price" }}
    />
  )
}

Props

  • pricedefault 0
    number

    The amount, formatted with locale and currency ("$489,000", "$1,284.90"). 0 hides the price row; pass summary for "Free" or anything else.

  • currencydefault "USD"
    string

    ISO 4217 code, shown with the narrow symbol ($, €, £). Empty shows the plain number.

  • localedefault ""
    string

    Intl locale for the price, like en-US. Empty uses the UIProvider locale, then the page's language. The demo uses en-US.

  • priceSuffixdefault ""
    string

    Smaller muted text right after the price: "/mo", "/night".

  • compareAtPricedefault 0
    number

    The old price, struck through at the start of the line under the price and read as "Was …" by screen readers. Shown only when it is above price.

  • badge
    ReactNode

    A small badge after the price, like <Badge tone="danger" variant="solid" size="sm">−25%</Badge>. It gives way to the price when a very narrow phone has no room for both.

  • captiondefault ""
    string

    One muted line under the price, like "Tax included · Free shipping". Hidden on phones under 360px wide.

  • actionLabeldefault "Continue"
    string

    The primary button's text. On a narrow phone a long label truncates before the price is ever cut.

  • summary
    ReactNode

    Replaces the whole price block: "2 tickets · Madison Square Garden", a seller, a sale badge.

  • actionIcon
    ReactNode

    An icon after the primary button's label, like an arrow.

  • onAction
    (() => void | Promise<unknown>)

    Runs on the primary button. Return a promise and the button shows its spinner (same width, aria-busy) until it settles.

  • actionProps
    Omit<ButtonProps, "children" | "onClick" | "size" | "tone" | "radius" | "color">

    Anything else for the primary Button: leftIcon, type="submit" with form, aria-*.

  • action
    ReactNode

    Replaces the primary button entirely, for example a <Button asChild> link to the checkout.

  • secondaryAction
    ReactNode

    An icon button before the primary: <Button variant="soft" size="icon" radius="full" aria-label="Save">. Match its size to the bar (icon-sm / icon / icon-lg).

  • variantdefault "bar"
    "bar" | "floating"

    bar: an edge-to-edge glass bar with a hairline on top. floating: an inset rounded card with a floating shadow, 12px above the safe area.

  • tonedefault "neutral"
    "neutral" | "brand" | "success" | "warning" | "danger" | "info"

    The primary button's color. Brand orange suits the one checkout action.

  • colordefault ""
    string

    Any CSS color for the primary button. Replaces the tone; the text turns black or white by lightness.

  • sizedefault "md"
    "sm" | "md" | "lg"

    Button size 32 / 40 / 48px (44px hit areas on touch), bar content height 56 / 64 / 72px, price 16 / 17 / 19px.

  • radiusdefault "md"
    "none" | "sm" | "md" | "lg" | "full"

    The buttons' corner shape, scaled with the size like the button's.

  • loadingdefault false
    boolean

    The primary button's spinner. It keeps its width and focus and ignores presses.

  • disableddefault false
    boolean

    Dims and blocks the primary action.

  • showOndefault "always"
    "always" | "scroll" | "anchor"

    always: on screen from the start. scroll: after threshold px of scrolling. anchor: while anchorRef (the page's own buy button) is out of view, not reached yet or scrolled past. Anything but always renders hidden on the server, so it never flashes.

  • thresholddefault 200
    number

    Scroll distance in px before the bar appears with showOn="scroll".

  • anchorRef
    RefObject<HTMLElement | null>

    The in-page buy button for showOn="anchor", watched with an IntersectionObserver on the scroll container. Without it the bar shows always (with a dev warning).

  • hideOnScrolldefault false
    boolean

    Slides away with the finger while scrolling down and comes back when scrolling up and at the end of the page.

  • keyboarddefault "hide"
    "hide" | "lift" | "none"

    While the on-screen keyboard is open: hide keeps a coupon field uncovered, lift rides on top of the keyboard (chat-like flows), none does nothing.

  • positiondefault "fixed"
    "fixed" | "sticky" | "absolute"

    fixed: to the viewport, above a bottom tab bar (bottom: var(--bottom-bar-offset)). sticky: the last child of a scroll container (the demo's phone frame). absolute: inside a relative box.

  • hideFromdefault "none"
    "none" | "md" | "lg"

    Hides the bar with CSS from this breakpoint up, where the page shows its own CTA column.

  • safeAreadefault true
    boolean

    Pads the home indicator area, minus what a bottom tab bar below already covers (no double padding), and the side insets of a landscape phone. env() is 0 on desktops.

  • scrollTarget
    HTMLElement | Window | RefObject<HTMLElement | null> | null

    What scrolls, for showOn, hideOnScroll and the anchor. Default: the nearest ScrollContainerProvider, else the window.

  • labels
    Partial<StickyCtaBarLabels>

    Screen reader strings: region (the landmark name, "Purchase"), compare (read before the old price, "Was"). DEFAULT_STICKY_CTA_BAR_LABELS holds the English defaults.

  • classNames
    Partial<Record<"inner" | "summary" | "price" | "suffix" | "compare" | "badge" | "caption" | "actions" | "secondary" | "primary", string>>

    Extra classes for the inner parts.

  • className
    string

    Extra classes for the root. The bar writes --cta-bar-offset (its visible height in px, 0 while hidden) on <html> when fixed, else on its parent, so floating buttons can sit above it.

Parts

  • DEFAULT_STICKY_CTA_BAR_LABELS

    constant

    The English screen reader strings: the landmark name ("Purchase") and the word read before the old price ("Was").

Examples

Checkout

A brand Pay button with a 1.4s request, a coupon that changes the total, and the keyboard behavior toggle.

Floating tickets

variant="floating" with a summary in place of the price.

Sale

price, compareAtPrice and a −25% badge.

Subscription

A /mo suffix and a price that fades when it changes.

Above a bottom tab bar

Both sticky; --cta-bar-offset and --bottom-bar-offset read live.

Show on scroll

showOn="scroll" with hideOnScroll on a listing feed.

Desktop CTA column

hideFrom="md" hands over to the page's own sidebar card.

Dependencies

motionbuttonnumber-formatuse-localeuse-scroll-stateuse-keyboard-insetmotiontonesui-config

Files

  • components/ui/sticky-cta-bar.tsx

FAQ

Is the React Sticky CTA Bar component free?

The preview, props and docs are free; copying the source needs a Pro plan. See plans.

How do I install Sticky CTA Bar?

Run npx wingo-ui@latest add sticky-cta-bar. It also adds the Wingo UI items it builds on (Button, Number format, useLocale, useScrollState, useKeyboardInset and 3 more) and the npm package motion. With shadcn, add the @wingo-ui registry with your API key, then run npx shadcn@latest add @wingo-ui/sticky-cta-bar.

See also

ButtonBottom Tab BaruseScrollStatehookuseKeyboardInsethookNumber formatlib

Used in

Checkout PageInvoice PageListing Detail PageHome Services PageJob Detail PageRestaurant PageTravel Page

More React mobile components

Slide To ConfirmAction SheetFABHaptic ButtonInstall PromptLarge Title Header
All 15 React mobile components

Articles using this component

  • React Checkout Page: Cart, Steps, Coupons and a Pay Bar

    Build it: screens and apps · Oct 16, 2026 · 12 min read

  • React Floating Action Button: Speed Dial and Safe Areas

    Mobile UI in React · Oct 16, 2026 · 9 min read

  • Mobile-First React Components: 9 Rules With Code

    Mobile UI in React · Oct 9, 2026 · 21 min read

  • Tailwind Safe Area Insets: Bottom Bars, FABs and Toasts

    Mobile UI in React · Oct 9, 2026 · 12 min read

PreviousSlide To ConfirmNext Swipe Deck
Bright living room with an open kitchen
1 / 12

New York, Upper West Side · Sep 24, 2026

2-bedroom apartment, separate rooms

$489,000$840/sq ft

580 sq ft
Area
2
Rooms
3 / 8
Floor
2012
Built

Description

A third-floor apartment in a 2012 building, with a bright living room, a separate kitchen, two bathrooms and an enclosed balcony over the courtyard. Own heating, double glazing, hardwood floors.

Five minutes from Central Park, with a bus stop and a school nearby. Sold furnished, with a parking spot included.

  • Own heating
  • Enclosed balcony
  • Parking spot
  • Elevator
  • Near Central Park
Ella MorganGreen House Realty · 38 listings

Similar listings

  • 2-bedroom apartment, Harlem$455,000New York, Harlem
  • Renovated studio, 370 sq ft$329,000New York, Chelsea
  • 3-bedroom apartment with terrace$612,000Jersey City, Downtown

Scroll the listing: the bar stays above the home indicator.

showOn always · threshold 200px · hideOnScroll off · keyboard hide · position fixed (sticky in this frame) · hideFrom none · safeArea on

Ella MorganVerified agent