How to Add a Notification Bar to Your Website With HTML, CSS, and JavaScript

A website notification bar is one of the highest-return components you can ship in an afternoon: a single strip at the top of the page that announces a sale, a shipping delay, a new release, or a maintenance window. Most articles about notification bars point you to a paid widget or a plugin that loads 80 KB of JavaScript. This guide does the opposite: you get roughly 40 lines of your own code, no dependencies, and full control over styling, dismissal logic and performance.

By the end of this tutorial you will have a bar that is sticky, dismissible, remembers the dismissal with localStorage, does not cause layout shift, and works with keyboards and screen readers.

What a website notification bar is (and when to use one)

A notification bar (also called an announcement bar, hello bar, sticky bar or top banner) is a full-width horizontal strip, usually pinned to the top or the bottom of the viewport, containing one short message and often one link or button.

Good use cases:

  • Promotions: “Free shipping over $50”, “20% off until Sunday”
  • Shipping and delivery notices: carrier delays, cut-off dates, holiday closures
  • Status and maintenance: scheduled downtime, incident updates
  • Product and content announcements: new version, new feature, webinar
  • Policy notices: pricing changes, terms updates

Bad use cases: anything that needs more than one sentence, anything that requires a decision (use a modal or a dedicated page), or three messages competing in the same bar.

website banner top bar

What we are building

Feature How it is handled
Sticks to the top on scroll position: sticky (no JS)
Dismissible Close button + Escape key
Stays dismissed localStorage with an ID and an expiry date
No flash, no layout shift Tiny blocking script in <head> that sets a class on <html>
Accessible Real <button>, labels, focus styles, 44px target
Weight Under 2 KB of HTML, CSS and JS combined

Step 1: The HTML markup

Place the bar as the first element inside <body>, before your header. Keeping it in the normal document flow is what lets us avoid layout shift later. 10 Website Notification Bar Examples that Get Results covers this in more depth.

<body>
  <div class="notice-bar" id="siteNotice" role="region" aria-label="Site announcement">
    <div class="notice-bar__inner">
      <p class="notice-bar__text">
        Free shipping on every order over $50.
        <a href="/shipping">See the details</a>
      </p>
      <button type="button" class="notice-bar__close" aria-label="Dismiss announcement">&times;</button>
    </div>
  </div>

  <header>...</header>
  <main id="main">...</main>
</body>

Three details that matter:

  • The close control is a real <button>, not a <div> or an <a href="#">. It is focusable and operable with Enter and Space for free.
  • aria-label="Dismiss announcement" gives screen reader users something better than “times”.
  • The wrapper uses role="region" with a label so the bar is reachable in a landmark list. Do not use role="alert" for a static promotional message; that is for urgent, dynamic content.

Step 2: The CSS (sticky, responsive, no jump)

:root {
  --notice-bg: #0f172a;
  --notice-fg: #ffffff;
  --notice-link: #7dd3fc;
}

.notice-bar {
  position: sticky;
  top: 0;
  z-index: 1000;
  background: var(--notice-bg);
  color: var(--notice-fg);
  font-size: 0.95rem;
  line-height: 1.4;
}

.notice-bar__inner {
  position: relative;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0.75rem 3.25rem 0.75rem 1rem;
}

.notice-bar__text {
  margin: 0;
  text-align: center;
}

.notice-bar__text a {
  color: var(--notice-link);
  text-decoration: underline;
}

.notice-bar__close {
  position: absolute;
  top: 50%;
  right: 0.25rem;
  transform: translateY(-50%);
  width: 44px;
  height: 44px;
  border: 0;
  border-radius: 6px;
  background: transparent;
  color: inherit;
  font-size: 1.5rem;
  line-height: 1;
  cursor: pointer;
}

.notice-bar__close:hover { background: rgba(255,255,255,0.12); }
.notice-bar__close:focus-visible { outline: 2px solid #fff; outline-offset: 2px; }

/* The dismissed state, driven by a class on <html> */
.notice-hidden .notice-bar { display: none; }

@media (max-width: 600px) {
  .notice-bar { font-size: 0.85rem; }
  .notice-bar__text { text-align: left; }
}

Why position: sticky instead of position: fixed

A sticky element still occupies space in the document flow. The rest of the page is laid out below it from the very first paint, so nothing is ever pushed down and your Cumulative Layout Shift stays at zero. A fixed element is removed from the flow and overlaps your header until you manually compensate with padding.

Positioning Stays visible on scroll Needs body padding Best for
static No No Low-priority notices
sticky Yes No Almost every case (recommended)
fixed Yes Yes Bars injected by a third-party script

If you really need a fixed bar

Measure it and expose the height as a CSS variable so the body can compensate without hard-coded numbers:

.notice-bar--fixed { position: fixed; top: 0; left: 0; right: 0; }
body { padding-top: var(--notice-height, 0px); }
html { scroll-padding-top: var(--notice-height, 0px); } /* anchors stay visible */
var bar = document.querySelector('.notice-bar--fixed');
function syncHeight() {
  document.documentElement.style.setProperty('--notice-height', bar.offsetHeight + 'px');
}
syncHeight();
if ('ResizeObserver' in window) { new ResizeObserver(syncHeight).observe(bar); }
window.addEventListener('resize', syncHeight);
website banner top bar

Step 3: The JavaScript (dismiss + localStorage)

Put this at the end of the body or in a deferred script file.

(function () {
  'use strict';

  var NOTICE_ID = 'free-shipping-2026-08'; // change this to bring the bar back
  var STORAGE_KEY = 'siteNotice';
  var REMEMBER_DAYS = 30;

  var bar = document.getElementById('siteNotice');
  if (!bar) { return; }

  function isDismissed() {
    try {
      var data = JSON.parse(localStorage.getItem(STORAGE_KEY) || 'null');
      return !!data && data.id === NOTICE_ID && data.expires > Date.now();
    } catch (e) {
      return false; // private mode, storage disabled, corrupted value
    }
  }

  function dismiss() {
    document.documentElement.classList.add('notice-hidden');
    try {
      localStorage.setItem(STORAGE_KEY, JSON.stringify({
        id: NOTICE_ID,
        expires: Date.now() + REMEMBER_DAYS * 86400000
      }));
    } catch (e) {}

    // keep keyboard users from losing their place
    var target = document.querySelector('main') || document.body;
    target.setAttribute('tabindex', '-1');
    target.focus({ preventScroll: true });
  }

  if (isDismissed()) {
    document.documentElement.classList.add('notice-hidden');
  }

  bar.querySelector('.notice-bar__close').addEventListener('click', dismiss);

  document.addEventListener('keydown', function (e) {
    if (e.key === 'Escape' && !document.documentElement.classList.contains('notice-hidden')) {
      dismiss();
    }
  });
})();

Why store an object instead of a simple boolean

  • The ID lets you publish a new message next month without every returning visitor staying blind to it. Change NOTICE_ID, the bar reappears for everyone.
  • The expiry stops a dismissal from lasting forever. Thirty days is a sensible default for promotions; use one or two days for shipping and status notices.
  • The try/catch matters more than people expect. localStorage throws in some privacy modes and when the quota is full. Failing silently means the bar simply shows again instead of breaking the page.

Step 4: Kill the flash and the layout shift

If you only hide the bar in the deferred script, returning visitors see the bar paint for a frame and then disappear, and everything below it jumps up. That is a real CLS penalty. The fix is a tiny synchronous script in the <head>, before your CSS renders anything:

<head>
  <script>
  (function () {
    try {
      var d = JSON.parse(localStorage.getItem('siteNotice') || 'null');
      if (d && d.id === 'free-shipping-2026-08' && d.expires > Date.now()) {
        document.documentElement.className += ' notice-hidden';
      }
    } catch (e) {}
  })();
  </script>
  <link rel="stylesheet" href="/style.css">
</head>

It runs in well under a millisecond, before first paint. The browser then lays out the page as if the bar never existed. This is the same technique used for dark-mode flash prevention, and it is the single biggest difference between a hand-rolled bar and most drop-in widgets, which inject the bar after load and shove your hero section down. A fuller account is out there.

Optional: animate the dismissal instead of snapping

.notice-bar {
  overflow: hidden;
  transition: grid-template-rows .25s ease;
}
.notice-bar.is-closing {
  opacity: 0;
  transition: opacity .18s ease;
}
@media (prefers-reduced-motion: reduce) {
  .notice-bar, .notice-bar.is-closing { transition: none; }
}

Add the class, wait for transitionend, then apply notice-hidden. Keep it under 250 ms; a slow closing animation feels broken.

Mobile considerations

  • One line, one idea. On a 360 px screen, a two-sentence message becomes four lines and eats a third of the viewport.
  • Shorten the copy with CSS or two spans, for example “Free shipping over $50” on mobile and the full sentence on desktop.
  • Keep the close button at 44 x 44 px minimum, and do not park it right next to the link, otherwise thumbs will dismiss the bar by accident.
  • Watch the safe area if you place the bar at the bottom: add padding-bottom: env(safe-area-inset-bottom);.
  • Stay well under Google’s intrusive interstitial threshold. A slim banner that leaves the main content visible is explicitly fine; a full-screen takeover on mobile is not.
website banner top bar

Accessibility checklist

  1. Contrast ratio of at least 4.5:1 for the text and 3:1 for the close icon against the bar background.
  2. Visible focus ring on the link and on the close button (:focus-visible).
  3. Descriptive aria-label on the close button.
  4. Move focus to <main> after dismissal so keyboard users do not get dropped at the top of the DOM.
  5. Use role="status" only if the bar appears after load (for example an incident notice pushed by your status API). A bar present in the initial HTML needs no live region.
  6. Never rely on color alone. “Delayed” in red is not enough; write the word.
  7. Respect prefers-reduced-motion for any slide or fade.

Useful variations

Show the bar only after the visitor scrolls

var sentinel = document.querySelector('#hero');
new IntersectionObserver(function (entries) {
  document.documentElement.classList.toggle('notice-hidden', entries[0].isIntersecting);
}, { threshold: 0 }).observe(sentinel);

Useful when you do not want to compete with the hero headline. Note that this variation does reserve no space at first, so pair it with a fixed bar rather than a sticky one.

Auto-expiring campaign bar

var END = new Date('2026-09-30T23:59:59Z').getTime();
if (Date.now() > END) { document.documentElement.classList.add('notice-hidden'); }

Set it once and the promotion disappears on its own instead of embarrassing you in October.

Rotating messages

Keep an array of two or three strings and swap the text every 6 to 8 seconds with a fade. Always give users a way to stop it, and never rotate faster than 5 seconds; WCAG 2.2.2 requires a pause control for moving content.

Where to paste this on real sites

Platform Where the markup goes
Static site / custom app Base template, first child of <body>
WordPress Child theme header.php after wp_body_open(), or hook into wp_body_open from functions.php; head script via wp_head
Shopify theme.liquid, right after the opening <body> tag
React / Next.js Root layout component; keep the head script inline so it runs before hydration
Express / Node (SSR) Layout view (EJS, Pug, Handlebars); pass the notice text and ID as template variables so marketing can change it without a deploy
website banner top bar

Common mistakes to avoid

  • No way to close it. A permanent bar trains people to ignore that entire strip of the page.
  • Stacking bars. Cookie banner + notification bar + sticky header on a phone leaves almost nothing above the fold.
  • Hard-coded pixel padding for a fixed bar. It breaks the moment the text wraps to two lines on a small screen.
  • Storing only true in localStorage, then wondering why the new campaign is invisible to your best returning visitors.
  • Loading a 100 KB widget to render a sentence. This whole component is smaller than a single icon font.
  • Vague copy. “Big news!” converts far worse than “Orders placed after Friday ship Sept 2”.

Measuring whether it works

Add a data attribute and fire one event on click and one on dismiss:

bar.querySelector('a').addEventListener('click', function () {
  if (window.dataLayer) { dataLayer.push({ event: 'notice_click', notice_id: NOTICE_ID }); }
});

Track three numbers: click-through rate, dismiss rate, and the conversion rate of sessions that clicked. A dismiss rate above 25 to 30 percent usually means the message is irrelevant to that audience, not that the bar is too visible.

FAQ

Does a notification bar hurt SEO?

Not by itself. A slim banner that leaves the main content readable is compliant with Google’s mobile guidance. What can hurt is the layout shift a badly implemented bar creates, which affects Core Web Vitals. The head-script technique in Step 4 removes that risk entirely.

Should I use localStorage or cookies to remember the dismissal?

localStorage is simpler, does not travel with every HTTP request and is enough for a purely cosmetic preference. Use a cookie only if your server needs to know the state to render the page (for example on a heavily cached CDN setup with edge personalization).

Do I need consent for storing the dismissal?

Storage that is strictly necessary for a user-requested function (here: remembering that the user closed a banner) generally falls under the functional exemption in the ePrivacy rules, and it holds no personal data. Consent becomes necessary the moment you attach analytics or advertising identifiers to it. Check with your own counsel for your jurisdiction. More at https://figma.com.

How do I make the bar come back after I publish a new message?

Change the NOTICE_ID value in both scripts. The stored ID no longer matches, so the bar shows again for everyone, including people who dismissed the previous one.

Sticky or fixed: which one should I pick?

Use sticky. It stays in the document flow, needs no compensating padding, keeps CLS at zero and works with a single CSS line. Reserve fixed for cases where the bar is injected dynamically after page load.

Can I put the bar at the bottom instead?

Yes, and it is often a better choice on mobile because it stays out of the way of the header and sits close to the thumb. Use position: fixed; bottom: 0;, add padding-bottom: env(safe-area-inset-bottom);, and make sure it does not cover a sticky add-to-cart button.

How many notification bars can I show at once?

One. If you have two things to say, pick the one that affects the visitor’s next action and put the other one on a page or in your footer.

Wrapping up

A useful website notification bar does not require a subscription or a plugin. Semantic HTML, one sticky rule, a versioned localStorage entry and a five-line head script give you a component that loads instantly, never shifts your layout, respects keyboard and screen reader users, and that you can restyle to match your brand in minutes. Copy the snippets above, swap the message and the NOTICE_ID, and ship it today.

Recent Posts

No Posts Found!

Categories

Tags

    Subscribe

    You have been successfully Subscribed! Ops! Something went wrong, please try again.

    About Us

    Express Jam Studio was founded in 2004 by John Smith. John had previously worked for a courier company, but he saw an opportunity to start his own business in the web design and development industry.

    Contact Info

    Copyright © 2022 Express Jam Studio. All Rights Reserved.