# Banner Announcement Marquee — React Banner Component

> A top strip that scrolls a seamless loop of announcements between faded edges, pauses on hover or focus and collapses when dismissed.

- Category: [Banners](https://pixelpapa.com/components/banners.md)
- Tags: banner, marquee, announcement, ticker, dismiss
- Dependencies: lucide-react, motion (plus React and Tailwind CSS v4)
- Last updated: 2026-11-13
- Live preview: https://pixelpapa.com/components/banners/banner-announcement-marquee

## Install

```bash
npx shadcn@latest add "https://pixelpapa.com/r/banner-announcement-marquee.json"
```

The file is written to `components/pixelpapa/banner-announcement-marquee.tsx`. To install manually, run `npm install lucide-react motion` and copy the source below into your project. It imports `cn` from `@/lib/utils`, which every shadcn project has.

## Source

```tsx
"use client";

import { Sparkles, X } from "lucide-react";
import { AnimatePresence, motion } from "motion/react";
import { useState } from "react";
import { cn } from "@/lib/utils";

type Item = { text: string; href: string };

type BannerAnnouncementMarqueeProps = {
  items?: Item[];
  /** Seconds for one full loop. */
  speed?: number;
  onDismiss?: () => void;
  className?: string;
};

const defaultItems: Item[] = [
  { text: "Free carbon-neutral shipping on orders over $60", href: "#" },
  { text: "The Linen Edit has landed — 14 new pieces", href: "#" },
  { text: "Members get early access to the archive sale on Friday", href: "#" },
  { text: "Now open in Copenhagen: Jægersborggade 41", href: "#" },
];

export function BannerAnnouncementMarquee({ items = defaultItems, speed = 36, onDismiss, className }: BannerAnnouncementMarqueeProps) {
  const [visible, setVisible] = useState(true);

  return (
    <AnimatePresence initial={false}>
      {visible && (
        <motion.div
          role="region"
          aria-label="Announcements"
          initial={{ height: 0 }}
          animate={{ height: 40 }}
          exit={{ height: 0 }}
          transition={{ type: "spring", stiffness: 300, damping: 34 }}
          className={cn("group relative overflow-hidden bg-foreground text-background", className)}
        >
          <style>{`
            @keyframes pp-banner-announcement-marquee-slide { to { transform: translate3d(-50%, 0, 0); } }
          `}</style>
          <div className="flex h-10 items-center [mask-image:linear-gradient(to_right,transparent,black_8%,black_86%,transparent_96%)]">
            <div
              className="flex w-max shrink-0 group-focus-within:[animation-play-state:paused] group-hover:[animation-play-state:paused] motion-reduce:[animation-play-state:paused]"
              style={{ animation: `pp-banner-announcement-marquee-slide ${speed}s linear infinite` }}
            >
              {[0, 1].map((copy) => (
                <ul key={copy} aria-hidden={copy === 1} className="flex shrink-0 items-center">
                  {items.map((item) => (
                    <li key={item.text} className="flex items-center text-sm whitespace-nowrap">
                      <a
                        href={item.href}
                        tabIndex={copy === 1 ? -1 : undefined}
                        className="px-6 underline-offset-4 outline-offset-[-2px] outline-background hover:underline"
                      >
                        {item.text}
                      </a>
                      <Sparkles aria-hidden className="size-3.5 opacity-50" />
                    </li>
                  ))}
                </ul>
              ))}
            </div>
          </div>
          <button
            type="button"
            aria-label="Dismiss announcements"
            onClick={() => {
              setVisible(false);
              onDismiss?.();
            }}
            className="absolute top-1.5 right-2 flex size-7 items-center justify-center rounded-full outline-background transition-colors hover:bg-background/15"
          >
            <X className="size-4" />
          </button>
        </motion.div>
      )}
    </AnimatePresence>
  );
}

const tiles = [
  { name: "Washed linen shirt", price: "$88", tint: "var(--chart-4)" },
  { name: "Wide-leg trouser", price: "$112", tint: "var(--chart-2)" },
  { name: "Canvas market tote", price: "$46", tint: "var(--chart-5)" },
  { name: "Merino crew", price: "$128", tint: "var(--chart-3)" },
];

export default function BannerAnnouncementMarqueeDemo() {
  const [round, setRound] = useState(0);
  const [dismissed, setDismissed] = useState(false);

  return (
    <div className="h-[440px] w-full overflow-hidden bg-background">
      <BannerAnnouncementMarquee key={round} onDismiss={() => setDismissed(true)} />
      <header className="mx-auto flex h-16 max-w-5xl items-center justify-between border-b border-border px-6">
        <span className="font-serif text-xl text-foreground">Nord &amp; Line</span>
        <nav aria-label="Main" className="hidden gap-7 text-sm text-muted-foreground sm:flex">
          {["New in", "Women", "Men", "Home"].map((l) => (
            <a key={l} href="#" className="hover:text-foreground">
              {l}
            </a>
          ))}
        </nav>
        <button
          type="button"
          onClick={() => {
            setDismissed(false);
            setRound((r) => r + 1);
          }}
          className={cn("text-sm text-muted-foreground underline-offset-4 hover:text-foreground hover:underline", !dismissed && "invisible")}
        >
          Show banner again
        </button>
      </header>
      <div className="mx-auto grid max-w-5xl grid-cols-2 gap-5 px-6 pt-8 sm:grid-cols-4">
        {tiles.map((t) => (
          <div key={t.name}>
            <div className="h-48 rounded-lg" style={{ background: `color-mix(in oklch, ${t.tint} 22%, var(--muted))` }} />
            <p className="mt-3 text-sm font-medium text-foreground">{t.name}</p>
            <p className="text-sm text-muted-foreground">{t.price}</p>
          </div>
        ))}
      </div>
    </div>
  );
}
```

## AI prompt

```text
Add the "Banner Announcement Marquee" component from PixelPapa UI to this project.

What it is: A top strip that scrolls a seamless loop of announcements between faded edges, pauses on hover or focus and collapses when dismissed.
Category: Banners. Depends on: lucide-react, motion.

1. Install it with the shadcn CLI:
   npx shadcn@latest add "https://pixelpapa.com/r/banner-announcement-marquee.json"
   (If the CLI is unavailable, fetch that URL, write files[0].content to components/pixelpapa/banner-announcement-marquee.tsx and install the listed dependencies.)
2. Import it from "@/components/pixelpapa/banner-announcement-marquee" and place it where I describe below.
3. Keep its animation timings and accessibility attributes. Replace the demo copy and data with content that fits my product, and use my existing theme tokens (bg-background, text-foreground, bg-primary, border-border) rather than hard-coded colours.

Where to use it: <describe the page and position here>
```

## More banners

- [Banner Cookie Consent](https://pixelpapa.com/components/banners/banner-cookie-consent.md) (Pro): A corner consent card whose preferences panel unfolds with spring height, with real switches per category and a chip to reopen it after a choice.
- [Banner Countdown Sale](https://pixelpapa.com/components/banners/banner-countdown-sale.md) (Pro): A sale strip with a ticking countdown whose digits roll over, a sweeping sheen, and a promo code that confirms itself when copied.
- [Banner Newsletter Inline](https://pixelpapa.com/components/banners/banner-newsletter-inline.md): An in-article signup banner: on submit the paper plane flies off, the field collapses and a confirmation line draws itself in.
- [Banner Status Incident](https://pixelpapa.com/components/banners/banner-status-incident.md) (Pro): A status strip with a pulsing severity dot that expands on a spring into a timeline of incident updates, recoloured by severity.
- [Banner Upgrade Progress](https://pixelpapa.com/components/banners/banner-upgrade-progress.md): An in-app usage banner whose meter springs to the current value, counts the number up and shifts from calm to amber to red as the plan limit nears.
