Announcement Bar
A slim top-of-page bar that rotates through announcements with a vertical slide, pauses on hover and can be dismissed.
$
View as Markdownnpx shadcn@latest add https://sundaeui.dev/r/announcement-bar.json'use client'
import { useEffect, useState } from 'react'
import './announcement-bar.css'
export interface Announcement { id: string; text: string; href?: string; cta?: string }
export interface AnnouncementBarProps {
messages: Announcement[]
interval?: number
onDismiss?: () => void
fill?: 'dark' | 'light'
className?: string
}
/** Rotating, dismissible announcement bar. */
export function AnnouncementBar({ messages, interval = 4000, onDismiss, fill = 'dark', className }: AnnouncementBarProps) {
const [i, setI] = useState(0)
const [dir, setDir] = useState<'up' | 'down'>('up')
const [paused, setPaused] = useState(false)
const [manual, setManual] = useState(false)
const [gone, setGone] = useState(false)
useEffect(() => {
if (paused || gone || messages.length < 2) return
const t = setTimeout(() => { setDir('up'); setManual(false); setI((x) => (x + 1) % messages.length) }, interval)
return () => clearTimeout(t)
}, [i, paused, gone, interval, messages.length])
const step = (d: 1 | -1) => { setDir(d > 0 ? 'up' : 'down'); setManual(true); setI((x) => (x + d + messages.length) % messages.length) }
const m = messages[i]
return (
<section
className={['sd-anb', className].filter(Boolean).join(' ')}
data-fill={fill}
data-gone={gone || undefined}
data-paused={paused || undefined}
aria-label="Announcements"
aria-hidden={gone || undefined}
onMouseEnter={() => setPaused(true)}
onMouseLeave={() => setPaused(false)}
onFocus={() => setPaused(true)}
onBlur={() => setPaused(false)}
>
<div className="sd-anb__in">
{messages.length > 1 && <button type="button" className="sd-anb__arr" aria-label="Previous announcement" onClick={() => step(-1)}><svg viewBox="0 0 12 12"><path d="M7.5 2.5 4 6l3.5 3.5" /></svg></button>}
<div className="sd-anb__win" aria-live={manual ? 'polite' : 'off'}>
<div key={`${m.id}-${i}`} className="sd-anb__msg" data-dir={dir}>
<span>{m.text}</span>{m.cta && <a href={m.href ?? '#'} onClick={(e) => (m.href ?? '#') === '#' && e.preventDefault()}>{m.cta}</a>}
</div>
{messages.length > 1 && <span key={`p${i}`} className="sd-anb__prog" style={{ animationDuration: `${interval}ms` }} aria-hidden />}
</div>
{messages.length > 1 && <button type="button" className="sd-anb__arr" aria-label="Next announcement" onClick={() => step(1)}><svg viewBox="0 0 12 12"><path d="M4.5 2.5 8 6 4.5 9.5" /></svg></button>}
<button type="button" className="sd-anb__x" aria-label="Dismiss announcements" onClick={() => { setGone(true); onDismiss?.() }}><svg viewBox="0 0 12 12"><path d="m3 3 6 6M9 3 3 9" /></svg></button>
</div>
</section>
)
}.sd-anb { --bg: #1c1c1c; --fg: #f6f6f6; --mute: #8e8e8e; --line: rgb(255 255 255 / 0.1); --soft: rgb(255 255 255 / 0.07); --strong: #fafafa; --on: #111;
color: var(--fg); font-size: 14px; line-height: 1.4; -webkit-tap-highlight-color: transparent; }
.sd-anb[data-fill='light'] { --bg: #fff; --fg: #121212; --mute: #6f6f6f; --line: rgb(0 0 0 / 0.09); --soft: rgb(0 0 0 / 0.05); --strong: #111; --on: #fff; }
.sd-anb button { font: inherit; color: inherit; background: none; border: 0; padding: 0; cursor: pointer; }
.sd-anb :focus-visible { outline: 2px solid var(--fg); outline-offset: 2px; }
.sd-anb .sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.sd-anb { width: min(100%, 360px); border-radius: 14px; background: var(--bg); box-shadow: inset 0 0 0 1px var(--line); overflow: hidden; display: grid; grid-template-rows: 1fr; transition: grid-template-rows 300ms ease, opacity 250ms; }
.sd-anb[data-gone] { grid-template-rows: 0fr; opacity: 0; }
.sd-anb__in { min-height: 0; display: flex; align-items: center; gap: 4px; padding: 0 6px; }
.sd-anb__arr, .sd-anb__x { display: grid; place-items: center; flex: none; width: 28px; height: 28px; border-radius: 8px; color: var(--mute); }
.sd-anb__arr:hover, .sd-anb__x:hover { color: var(--fg); background: var(--soft) !important; }
.sd-anb svg { width: 12px; height: 12px; fill: none; stroke: currentColor; stroke-width: 1.8; stroke-linecap: round; stroke-linejoin: round; }
.sd-anb__win { position: relative; flex: 1; min-width: 0; height: 44px; overflow: hidden; }
.sd-anb__msg { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; gap: 8px; font-size: 13px; text-align: center; white-space: nowrap; animation: sd-anb-in 420ms cubic-bezier(0.2, 1, 0.3, 1); }
.sd-anb__msg[data-dir='down'] { animation-name: sd-anb-in-down; }
.sd-anb__msg span { overflow: hidden; text-overflow: ellipsis; }
.sd-anb__msg a { flex: none; color: var(--fg); font-weight: 600; text-decoration: underline; text-underline-offset: 3px; }
@keyframes sd-anb-in { from { transform: translateY(100%); opacity: 0; } }
@keyframes sd-anb-in-down { from { transform: translateY(-100%); opacity: 0; } }
.sd-anb__prog { position: absolute; left: 0; bottom: 0; height: 2px; background: var(--fg); opacity: 0.25; animation: sd-anb-prog linear forwards; }
.sd-anb[data-paused] .sd-anb__prog { animation-play-state: paused; }
@keyframes sd-anb-prog { from { width: 0; } to { width: 100%; } }
.sd-anb__undo { margin-top: 12px; font-size: 12.5px; color: var(--mute); text-decoration: underline; }
@media (prefers-reduced-motion: reduce) { .sd-anb *, .sd-anb *::before, .sd-anb *::after { animation: none !important; transition: none !important; } }Overview
Messages slide up one after another on a timer and pause while the pointer or focus is on the bar. Small arrows let people step through manually.
Dismiss collapses the bar’s height smoothly so the page below does not jump.
When to use
- Launches, sales, status notes, cookie-free promo slots.
When not to use
- Urgent alerts: use an inline alert that does not rotate away.
Installation
$
pnpm dlx shadcn@latest add https://sundaeui.dev/r/announcement-bar.json$
npx shadcn@latest add https://sundaeui.dev/r/announcement-bar.json$
yarn dlx shadcn@latest add https://sundaeui.dev/r/announcement-bar.json$
bunx --bun shadcn@latest add https://sundaeui.dev/r/announcement-bar.jsonWorks in any React project. Files are written to components/sundae/. Read the source in the stage’s Code view.
- No dependencies beyond React 18 or later.
- Copy announcement-bar.tsx and announcement-bar.css into components/sundae/.
- The CSS file is imported by the component, so no extra configuration is needed.
- Import the component and use it as shown below.
Usage
import { AnnouncementBar } from '@/components/sundae/announcement-bar'
<AnnouncementBar />Props
| Prop | Type | Default |
|---|---|---|
messages*Announcements. | { id: string; text: string; href?: string; cta?: string }[] | — |
intervalRotation in ms. | number | 4000 |
onDismissCalled on close. | () => void | — |
fillSurface: dark or light. | 'dark' | 'light' | 'dark' |
Accessibility
- A region labelled "Announcements"; rotation pauses on hover and focus (WCAG 2.2.2).
- The message is announced politely only when changed manually.
- Reduced motion removes the movement and keeps every state change.
Customization
- Colours are CSS custom properties on .sd-anb; override them in your own stylesheet.
- fill="light" switches the built-in light palette.
FAQ
- Is Announcement Bar free?
- Yes. Announcement Bar is free for personal and commercial projects, with full source on this page.
- How do I install Announcement Bar?
- Run npx shadcn@latest add https://sundaeui.dev/r/announcement-bar.json. The files are written to components/sundae/. It has no dependencies beyond React 18 or later.
- Does Announcement Bar support keyboard and screen readers?
- A region labelled "Announcements"; rotation pauses on hover and focus (WCAG 2.2.2). The message is announced politely only when changed manually.
- Does Announcement Bar respect reduced motion?
- Reduced motion removes the movement and keeps every state change.
More like this
All components →Cookie Consent
Components · RefinedInline Alert
Components · RefinedToast Stack
Components · Refined- Pro
Sheet Drawer
Components · Refined Avatar Stack
Components · Refined- Pro
Notification Center
Components · Refined