Innovations

Sticky Announcement Bar

Fixed top CTA bar that persists while scrolling. Dismissable with localStorage memory so it stays gone after close.

Preview

Source

tsx
"use client";

import { useState, useEffect } from "react";
import { Button } from "@/components/ui/button";
import { X, Sparkles } from "lucide-react";

const STORAGE_KEY = "innovations_sticky_bar_dismissed";

export default function StickyBar() {
  const [visible, setVisible] = useState(false);

  // SSR-guarded: check localStorage only in the browser
  useEffect(() => {
    const dismissed = localStorage.getItem(STORAGE_KEY);
    if (!dismissed) {
      setVisible(true);
    }
  }, []);

  function dismiss() {
    setVisible(false);
    localStorage.setItem(STORAGE_KEY, "true");
  }

  if (!visible) {
    return (
      <div className="flex min-h-[200px] flex-col items-center justify-center gap-4 p-8 text-center">
        <p className="text-sm text-muted-foreground">
          Sticky bar is dismissed.{" "}
          <button
            onClick={() => {
              localStorage.removeItem(STORAGE_KEY);
              setVisible(true);
            }}
            className="text-primary underline hover:no-underline"
          >
            Reset demo
          </button>
        </p>
      </div>
    );
  }

  return (
    <>
      {/* Sticky bar */}
      <div className="fixed top-0 left-0 right-0 z-50 flex items-center justify-between gap-4 bg-primary px-4 py-3 sm:px-6">
        <div className="flex items-center gap-2 text-primary-foreground min-w-0">
          <Sparkles className="h-4 w-4 shrink-0 opacity-80" />
          <p className="text-sm font-medium truncate">
            <span className="font-bold">Limited time:</span> Get our free growth playbook — no strings attached.
          </p>
        </div>

        <div className="flex items-center gap-2 shrink-0">
          <Button
            size="sm"
            variant="secondary"
            className="h-7 text-xs font-semibold hidden sm:inline-flex"
            asChild
          >
            <a href="#optin">Grab it free</a>
          </Button>
          <button
            onClick={dismiss}
            aria-label="Dismiss"
            className="rounded-full p-1 text-primary-foreground/70 transition-colors hover:bg-white/20 hover:text-primary-foreground"
          >
            <X className="h-4 w-4" />
          </button>
        </div>
      </div>

      {/* Spacer to prevent content jump */}
      <div className="h-11" />

      {/* Preview content */}
      <div className="flex min-h-[180px] items-center justify-center p-8">
        <p className="text-sm text-muted-foreground text-center max-w-xs">
          Scroll down — the bar stays pinned to the top. Dismiss it with ✕ and it won't reappear (localStorage).
        </p>
      </div>
    </>
  );
}
Claude Code Instructions

CLI Install

npx innovations add sticky-bar

Where to use it

Add to your root layout so it appears on every page. In Next.js root layout (app/layout.tsx): import StickyBar from '@/components/innovations/freebies/sticky-bar'; // Place as the first child inside <body> In Astro root layout: <StickyBar client:load /> The localStorage check is inside useEffect so it's SSR-safe. Change STORAGE_KEY to a unique string per promotion so returning visitors see new bars. The bar uses bg-primary — override with any Tailwind background class to match your brand. Add padding-top to your main content container (pt-11 or pt-14) to prevent the bar from overlapping content.