Store Navbar with Announcement Bar and Category Row

A three-layer header for an online shop. A dark announcement bar with a link and a dismiss button sits on top. The main row holds a serif wordmark, a wide search field, and account, saved and basket links with an item count. The second row lists departments, underlines the current one and colours Sale differently. On phones the search drops to its own line and the department row scrolls sideways inside its own strip, so the page itself never scrolls sideways.

Code

import { useState } from "react";

const categories = [
  { label: "New in", href: "#" },
  { label: "Kitchen", href: "#", current: true },
  { label: "Tableware", href: "#" },
  { label: "Bedding", href: "#" },
  { label: "Bath", href: "#" },
  { label: "Lighting", href: "#" },
  { label: "Garden", href: "#" },
  { label: "Sale", href: "#", sale: true },
];

const cartCount = 3;

export default function StoreNavbarWithAnnouncementBar() {
  const [showNotice, setShowNotice] = useState(true);
  const focus = "focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-700 focus-visible:ring-offset-2";
  const iconLink = `relative inline-flex h-11 min-w-[44px] items-center justify-center gap-2 px-2 text-sm font-medium text-stone-800 transition hover:text-orange-800 ${focus}`;

  return (
    <header className="bg-stone-50 text-stone-900">
      {showNotice && (
        <div className="bg-stone-900 text-stone-100">
          <div className="mx-auto flex max-w-6xl items-center gap-3 px-4 sm:px-6">
            <p className="flex-1 py-2.5 text-left text-sm sm:text-center">
              Free delivery on orders over $60 until Sunday.{" "}
              <a href="#" className="whitespace-nowrap font-semibold underline decoration-orange-400 decoration-2 underline-offset-4 focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-400">
                Shop the edit
              </a>
            </p>
            <button
              type="button"
              aria-label="Dismiss announcement"
              onClick={() => setShowNotice(false)}
              className="-mr-2 inline-flex h-10 w-10 flex-none items-center justify-center text-stone-300 hover:text-white focus:outline-none focus-visible:ring-2 focus-visible:ring-orange-400"
            >
              <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
                <path d="M5 5l10 10M15 5 5 15" stroke="currentColor" strokeWidth="1.8" strokeLinecap="round" />
              </svg>
            </button>
          </div>
        </div>
      )}

      <div className="mx-auto grid max-w-6xl grid-cols-[1fr_auto] items-center gap-x-6 gap-y-3 px-4 py-4 sm:px-6 md:grid-cols-[auto_1fr_auto] md:gap-x-10 md:py-5">
        <a href="#" className={`w-fit font-serif text-2xl font-semibold tracking-tight ${focus}`}>
          Oat <span className="text-orange-700">&amp;</span> Ember
        </a>

        <form action="#" role="search" className="order-last col-span-2 flex md:order-none md:col-span-1">
          <label htmlFor="store-search" className="sr-only">Search the store</label>
          <input
            id="store-search"
            type="search"
            placeholder="Search for linen, mugs, lamps"
            className="h-11 w-full min-w-0 border border-r-0 border-stone-300 bg-white px-4 text-sm placeholder:text-stone-500 focus:border-stone-900 focus:outline-none"
          />
          <button type="submit" aria-label="Search" className={`inline-flex h-11 w-12 flex-none items-center justify-center bg-stone-900 text-white transition hover:bg-orange-700 ${focus}`}>
            <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-5 w-5">
              <circle cx="9" cy="9" r="5.5" stroke="currentColor" strokeWidth="1.7" />
              <path d="m13.5 13.5 3.5 3.5" stroke="currentColor" strokeWidth="1.7" strokeLinecap="round" />
            </svg>
          </button>
        </form>

        <div className="flex items-center justify-end md:gap-2">
          <a href="#" aria-label="Account" className={iconLink}>
            <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-6 w-6">
              <circle cx="12" cy="8.5" r="3.5" stroke="currentColor" strokeWidth="1.6" />
              <path d="M5 20c.6-3.6 3.4-5.5 7-5.5s6.4 1.9 7 5.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
            </svg>
            <span aria-hidden="true" className="hidden lg:inline">Account</span>
          </a>
          <a href="#" aria-label="Saved items" className={iconLink}>
            <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-6 w-6">
              <path d="M12 19.5S4.5 15.2 4.5 9.8A3.8 3.8 0 0 1 12 8.3a3.8 3.8 0 0 1 7.5 1.5c0 5.4-7.5 9.7-7.5 9.7Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
            </svg>
            <span aria-hidden="true" className="hidden lg:inline">Saved</span>
          </a>
          <a href="#" aria-label={`Basket, ${cartCount} items`} className={iconLink}>
            <span className="relative">
              <svg viewBox="0 0 24 24" fill="none" aria-hidden="true" className="h-6 w-6">
                <path d="M5 8.5h14l-1.2 11H6.2L5 8.5Z" stroke="currentColor" strokeWidth="1.6" strokeLinejoin="round" />
                <path d="M9 8.5V7a3 3 0 0 1 6 0v1.5" stroke="currentColor" strokeWidth="1.6" strokeLinecap="round" />
              </svg>
              <span aria-hidden="true" className="absolute -right-2 -top-1.5 flex h-[18px] min-w-[18px] items-center justify-center rounded-full bg-orange-700 px-1 text-[11px] font-bold leading-none text-white">
                {cartCount}
              </span>
            </span>
            <span aria-hidden="true" className="hidden lg:inline">Basket</span>
          </a>
        </div>
      </div>

      <nav aria-label="Departments" className="border-y border-stone-200 bg-white">
        <div className="mx-auto flex max-w-6xl items-center px-4 sm:px-6">
          <ul className="-mx-3 flex flex-1 items-center overflow-x-auto">
            {categories.map((c) => (
              <li key={c.label} className="flex-none">
                <a
                  href={c.href}
                  aria-current={c.current ? "page" : undefined}
                  className={`relative inline-flex h-12 items-center px-3 text-sm font-medium transition focus:outline-none focus-visible:ring-2 focus-visible:ring-inset focus-visible:ring-orange-700 ${c.sale ? "text-orange-700 hover:text-orange-800" : c.current ? "text-stone-900" : "text-stone-600 hover:text-stone-900"}`}
                >
                  {c.label}
                  {c.current && <span aria-hidden="true" className="absolute inset-x-3 bottom-0 h-0.5 bg-stone-900" />}
                </a>
              </li>
            ))}
          </ul>
          <a href="#" className={`hidden h-12 flex-none items-center gap-2 pl-6 text-sm text-stone-600 hover:text-stone-900 lg:inline-flex ${focus}`}>
            <svg viewBox="0 0 20 20" fill="none" aria-hidden="true" className="h-4 w-4">
              <path d="M10 17.5s5-4.3 5-8.5a5 5 0 0 0-10 0c0 4.2 5 8.5 5 8.5Z" stroke="currentColor" strokeWidth="1.5" strokeLinejoin="round" />
              <circle cx="10" cy="9" r="1.8" stroke="currentColor" strokeWidth="1.5" />
            </svg>
            Find a shop
          </a>
        </div>
      </nav>
    </header>
  );
}

Install with one command: npx shadcn@latest add https://we.inc/r/store-navbar-with-announcement-bar.json

When to use it

How to customize it

Accessibility

Questions

What is an announcement bar?

It is a thin strip above the header that carries one short message, such as a delivery offer, a sale date or a shipping cut-off. Keep it to a single sentence and one link, and let visitors dismiss it.

How do I keep the announcement bar dismissed on the next visit?

The component keeps the choice in `useState`, which resets on reload. To remember it, save a flag when the button is clicked and read that flag when setting the initial state. Change the flag name whenever the message changes so new offers show again.

Why are there two navigation rows?

Shops need room for search and the basket as well as a long list of departments. Splitting them into two rows gives the search field real width and lets the department list grow without crowding the icons.

How does the category row work on phones?

The list has `overflow-x-auto` and each item has `flex-none`, so the links keep their natural width and the row scrolls sideways under the thumb. Nothing wraps onto a second line and the page width stays fixed.

How do I make the header sticky?

Add `sticky top-0 z-40` to the `<header>`. On phones consider making only the main row sticky, because three sticky layers take a large share of a small screen.

Related: Navbar with Centered Links, CTA Button and Mobile Menu, Dark Navbar with Dropdown Menu and Search Field, Footer with Four Link Columns and Newsletter Signup, Button Set: Primary, Secondary, Ghost, Icon and Loading.

More navbars or all free UI components. Start for free to have a full site built around it.

More in Navbars

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