Announcement Banner
blockmarketingInteractive release, security, and maintenance broadcast center with rotating live ticker, CLI copy button, and floating pill / top-bar modes.
Installation
$pnpm dlx shadcn@latest add https://uipkge.dev/r/react/announcement-banner.json$npx shadcn@latest add https://uipkge.dev/r/react/announcement-banner.json$yarn dlx shadcn@latest add https://uipkge.dev/r/react/announcement-banner.json$bunx shadcn@latest add https://uipkge.dev/r/react/announcement-banner.jsonNamed registry:
npx shadcn@latest add @uipkge-react/announcement-bannerInstalls to:components/blocks/Variants
Loading interactive previews…
Loading interactive previews…
Props
| Name | Type / Values | Default | Required |
|---|---|---|---|
variant | 'floating-pill' | 'top-bar' | 'interactive-ticker' | — | optional |
autoplay | boolean | — | optional |
className | string | — | optional |
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
AnnouncementIteminterface AnnouncementItem {
id: string
category: 'release' | 'security' | 'maintenance' | 'feature'
badgeText: string
title: string
actionLabel: string
actionUrl?: string
cliSnippet?: string
}npm dependencies
Includes
Files installed (1)
components/blocks/AnnouncementBanner.tsx9.7 kB
'use client' import * as React from 'react' import { ArrowRight, ChevronLeft, ChevronRight, Megaphone, Terminal, X } from 'lucide-react' import { Badge } from '@/components/ui/badge' import { cn } from '@/lib/utils' export interface AnnouncementItem { id: string category: 'release' | 'security' | 'maintenance' | 'feature' badgeText: string title: string actionLabel: string actionUrl?: string cliSnippet?: string } export interface AnnouncementBannerProps { variant?: 'floating-pill' | 'top-bar' | 'interactive-ticker' autoplay?: boolean className?: string } const announcements: AnnouncementItem[] = [ { id: 'ann-1', category: 'release', badgeText: 'v2.4.0 Release', title: 'Tailwind CSS v4 OKLCH tokens & 281+ production workbenches are live.', actionLabel: 'Read Changelog', actionUrl: 'https://uipkge.dev/changelog', cliSnippet: 'npx shadcn-vue@latest add https://uipkge.dev/r/vue/init.json', }, { id: 'ann-2', category: 'feature', badgeText: 'Dual-Framework', title: '100% Vue 3.5 & React 19 component parity achieved with zero runtime CSS.', actionLabel: 'Explore Parity Matrix', actionUrl: 'https://uipkge.dev/components', cliSnippet: 'npx shadcn@latest add https://uipkge.dev/r/react/init.json', }, { id: 'ann-3', category: 'security', badgeText: 'Security Notice', title: 'Upstream Reka UI & Radix dependency security patches verified and updated.', actionLabel: 'Security Advisory', actionUrl: 'https://uipkge.dev/security', }, ] export function AnnouncementBanner({ variant = 'interactive-ticker', autoplay = true, className, }: AnnouncementBannerProps) { const [currentIndex, setCurrentIndex] = React.useState(0) const [dismissed, setDismissed] = React.useState(false) const [isPaused, setIsPaused] = React.useState(false) const [copiedCli, setCopiedCli] = React.useState(false) React.useEffect(() => { if (!autoplay || isPaused) return const timer = setInterval(() => { setCurrentIndex((prev) => (prev + 1) % announcements.length) }, 6000) return () => clearInterval(timer) }, [autoplay, isPaused]) const nextAnnouncement = () => { setCurrentIndex((prev) => (prev + 1) % announcements.length) } const prevAnnouncement = () => { setCurrentIndex((prev) => (prev - 1 + announcements.length) % announcements.length) } const copyCliSnippet = (snippet?: string) => { if (!snippet) return navigator.clipboard.writeText(snippet) setCopiedCli(true) setTimeout(() => setCopiedCli(false), 2000) } if (dismissed) return null const current = announcements[currentIndex] return ( <div data-slot="announcement-banner" data-variant={variant} role="region" aria-label="Announcement" className={cn('relative z-50 transition-colors duration-200', className)} onMouseEnter={() => setIsPaused(true)} onMouseLeave={() => setIsPaused(false)} > {/* 1. Interactive Ticker Mode (Default) */} {variant === 'interactive-ticker' && ( <div className="bg-muted/70 border-border text-foreground flex items-center justify-between gap-3 overflow-hidden border-b px-4 py-2 text-xs shadow-xs backdrop-blur-md"> <div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-4"> {/* Left: Status Beacon & Item Indicator */} <div className="flex shrink-0 items-center gap-3"> <div className="text-muted-foreground flex items-center gap-1.5 font-mono text-xs"> <span className="bg-primary size-2 animate-pulse rounded-full" /> <span className="text-foreground font-semibold">Live Dispatch</span> </div> <Badge variant="secondary" className={cn( 'px-2 py-0.5 font-mono text-xs', current.category === 'release' && 'bg-primary/10 text-primary border-primary/20', current.category === 'security' && 'border-warning/20 bg-warning/10 text-warning', current.category === 'feature' && 'border-success/20 bg-success/10 text-success', )} > {current.badgeText} </Badge> </div> {/* Center: Dynamic Headline & Action Link */} <div className="flex flex-1 items-center justify-center gap-2 truncate"> <p className="text-foreground truncate font-medium">{current.title}</p> {current.actionUrl && ( <a href={current.actionUrl} className="text-primary hidden shrink-0 items-center gap-1 font-semibold hover:underline sm:inline-flex" > {current.actionLabel} <ArrowRight className="size-3" /> </a> )} </div> {/* Right: Ticker Navigation & CLI / Dismiss */} <div className="flex shrink-0 items-center gap-2"> {current.cliSnippet && ( <button type="button" className="bg-background border-border text-muted-foreground hover:text-foreground hidden items-center gap-1.5 rounded border px-2 py-0.5 font-mono text-xs transition-colors md:inline-flex" onClick={() => copyCliSnippet(current.cliSnippet)} > <Terminal className="text-primary size-3" /> <span>{copiedCli ? 'Copied CLI!' : 'Copy CLI'}</span> </button> )} <div className="border-border bg-background flex items-center overflow-hidden rounded-md border"> <button type="button" className="hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors" aria-label="Previous announcement" onClick={prevAnnouncement} > <ChevronLeft className="size-3.5" /> </button> <span className="text-muted-foreground px-1.5 font-mono text-xs"> {currentIndex + 1}/{announcements.length} </span> <button type="button" className="hover:bg-muted text-muted-foreground hover:text-foreground p-1 transition-colors" aria-label="Next announcement" onClick={nextAnnouncement} > <ChevronRight className="size-3.5" /> </button> </div> <button type="button" aria-label="Dismiss banner" className="text-muted-foreground hover:text-foreground hover:bg-muted rounded-md p-1 transition-colors" onClick={() => setDismissed(true)} > <X className="size-3.5" /> </button> </div> </div> </div> )} {/* 2. Floating Pill Mode */} {variant === 'floating-pill' && ( <div className="mx-auto w-fit max-w-2xl px-4 py-2"> <div className="bg-card/90 border-border text-foreground hover:border-primary/40 flex items-center gap-3 rounded-full border py-1.5 pr-2 pl-3 text-xs shadow-md backdrop-blur-md transition-colors"> <Badge variant="secondary" className="bg-primary/10 text-primary border-primary/20 px-2 py-0.5 font-mono text-xs" > {current.badgeText} </Badge> <p className="text-foreground max-w-sm truncate font-medium">{current.title}</p> {current.actionUrl && ( <a href={current.actionUrl} className="text-primary inline-flex shrink-0 items-center gap-1 font-semibold hover:underline" > {current.actionLabel} <ArrowRight className="size-3" /> </a> )} <button type="button" aria-label="Dismiss announcement" className="text-muted-foreground hover:text-foreground hover:bg-muted rounded-full p-1 transition-colors" onClick={() => setDismissed(true)} > <X className="size-3.5" /> </button> </div> </div> )} {/* 3. Top Solid Bar Mode */} {variant === 'top-bar' && ( <div className="bg-primary text-primary-foreground flex items-center justify-between gap-3 px-4 py-2 text-xs shadow-xs"> <div className="mx-auto flex w-full max-w-6xl items-center justify-between gap-4"> <div className="flex items-center gap-2 truncate"> <Megaphone className="size-4 shrink-0" /> <span className="font-mono font-bold">[{current.badgeText}]</span> <span className="truncate">{current.title}</span> </div> <div className="flex shrink-0 items-center gap-3"> {current.actionUrl && ( <a href={current.actionUrl} className="inline-flex items-center gap-1 font-semibold underline-offset-4 hover:underline" > {current.actionLabel} <ArrowRight className="size-3" /> </a> )} <button type="button" aria-label="Dismiss banner" className="text-primary-foreground/80 hover:text-primary-foreground hover:bg-primary-foreground/15 rounded p-1 transition-colors" onClick={() => setDismissed(true)} > <X className="size-3.5" /> </button> </div> </div> </div> )} </div> ) } export default AnnouncementBanner
Raw manifest:https://uipkge.dev/r/react/announcement-banner.json