Sticky Bottom Offer Bar

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.

Code

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

When to use it

How to customize it

Accessibility

Questions

How do I make a bar stick to the bottom of the screen in Tailwind?

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.

Will a sticky bar cover my footer or cookie notice?

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.

Should visitors be able to close it?

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.

Top bar or bottom bar?

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.

Why is the preview inside a frame?

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.

More in Cta Sections

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.

Product

Who It's For

Features

Resources

Company

View Sitemap