A dark offer bar that sits along the bottom edge of the page while the visitor scrolls. It holds a small deadline tag, one line about the offer with a discount code, a button and a close control. Closing it leaves a small "Show offer" button so the preview can bring it back. The bar is drawn inside a mock browser frame here; on a real page you fix it to the window. On phones the text wraps above a full-width button and the close control moves to the top corner.
import { useState } from "react";
const offer = {
tag: "Ends Sunday",
text: "25% off every annual plan.",
detail: "Use code SPRING25 at checkout.",
cta: "Claim the discount",
};
export default function StickyBottomOfferBar() {
const [open, setOpen] = useState(true);
return (
<div className="bg-slate-100 p-4 sm:p-10">
{/* The frame stands in for a browser window. On a real page, drop the frame and make the bar `fixed inset-x-0 bottom-0`. */}
<div className="relative mx-auto h-[540px] max-w-4xl overflow-hidden rounded-xl border border-slate-200 bg-white shadow-sm">
<div aria-hidden="true">
<div className="flex items-center gap-2 border-b border-slate-200 bg-slate-50 px-4 py-3">
<span className="h-2.5 w-2.5 rounded-full bg-slate-300" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-300" />
<span className="h-2.5 w-2.5 rounded-full bg-slate-300" />
<span className="ml-3 h-6 flex-1 rounded-md bg-white ring-1 ring-slate-200" />
</div>
<div className="px-6 py-6 sm:px-10">
<div className="flex items-center justify-between">
<span className="h-4 w-24 rounded bg-slate-800" />
<div className="hidden gap-5 sm:flex">
<span className="h-2.5 w-14 rounded-full bg-slate-200" />
<span className="h-2.5 w-14 rounded-full bg-slate-200" />
<span className="h-2.5 w-14 rounded-full bg-slate-200" />
</div>
</div>
<div className="mt-10 h-7 w-4/5 rounded bg-slate-200 sm:w-3/5" />
<div className="mt-3 h-7 w-3/5 rounded bg-slate-200 sm:w-2/5" />
<div className="mt-6 space-y-2.5">
<div className="h-2.5 w-full rounded-full bg-slate-100" />
<div className="h-2.5 w-11/12 rounded-full bg-slate-100" />
<div className="h-2.5 w-3/4 rounded-full bg-slate-100" />
</div>
<div className="mt-8 grid gap-4 sm:grid-cols-3">
<div className="h-28 rounded-lg bg-slate-100" />
<div className="h-28 rounded-lg bg-slate-100" />
<div className="hidden h-28 rounded-lg bg-slate-100 sm:block" />
</div>
</div>
</div>
{open ? (
<aside aria-label="Limited time offer" className="absolute inset-x-0 bottom-0 bg-slate-900 text-white shadow-[0_-8px_24px_rgba(15,23,42,0.18)]">
<div className="flex flex-col gap-3 px-4 py-4 sm:flex-row sm:items-center sm:gap-4 sm:px-6 sm:py-3">
<div className="flex flex-1 flex-col gap-2 pr-10 sm:flex-row sm:items-center sm:gap-3 sm:pr-0">
<span className="w-fit flex-none rounded-full bg-rose-600 px-2.5 py-1 text-xs font-semibold text-white">{offer.tag}</span>
<p className="text-sm leading-relaxed">
<span className="font-semibold">{offer.text}</span> <span className="text-slate-300">{offer.detail}</span>
</p>
</div>
<a
href="#"
className="inline-flex h-10 flex-none items-center justify-center rounded-lg bg-white px-4 text-sm font-semibold text-slate-900 transition hover:bg-slate-200 focus:outline-none focus-visible:ring-2 focus-visible:ring-white focus-visible:ring-offset-2 focus-visible:ring-offset-slate-900"
>
{offer.cta}
</a>
<button
type="button"
aria-label="Dismiss offer"
onClick={() => setOpen(false)}
className="absolute right-2 top-2 inline-flex h-10 w-10 flex-none items-center justify-center rounded-lg text-slate-300 transition hover:bg-slate-800 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-white sm:static"
>
<svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-5 w-5">
<path d="M5.5 5.5l9 9m0-9l-9 9" stroke="currentColor" strokeWidth="2" strokeLinecap="round" />
</svg>
</button>
</div>
</aside>
) : (
<button
type="button"
onClick={() => setOpen(true)}
className="absolute bottom-4 right-4 inline-flex h-10 items-center rounded-full bg-slate-900 px-4 text-sm font-semibold text-white shadow-lg transition hover:bg-slate-700 focus:outline-none focus-visible:ring-2 focus-visible:ring-slate-900 focus-visible:ring-offset-2"
>
Show offer
</button>
)}
</div>
</div>
);
}
Install with one command: npx shadcn@latest add https://we.inc/r/sticky-bottom-offer-bar.json
Use `fixed inset-x-0 bottom-0` with a `z-` class high enough to sit above your content. `sticky bottom-0` also works when the bar is the last child of a tall scrolling container.
It can. Add padding to the bottom of the page that matches the bar height, and if you also show a cookie notice, show one at a time or stack them with different `bottom-` values.
Yes. A bar that cannot be dismissed takes screen space on every page, which is most costly on phones. Store the dismissal so it stays closed for that visitor.
A top bar is seen first on desktop. A bottom bar stays visible during scrolling and is easier to reach on a phone. For an offer with a button, the bottom usually suits phone traffic better.
A fixed element would attach to the whole browser window and escape the preview box. Drawing it inside a positioned frame with a set height shows where it sits without affecting the surrounding page.
Related: Call-to-Action Banner on a Brand Colour, Newsletter Signup with Benefit List, Button Set: Primary, Secondary, Ghost, Icon and Loading.
More call-to-action sections or all free UI components. Start for free to have a full site built around it.
We.Inc is an AI-powered website builder you can resell under your own brand. Launch a branded client dashboard, bill on Stripe Connect, and deliver AI-generated websites in minutes. White-label plans start at $99 a month for 25 client sites, with a 7-day free trial and no per-site fees.