Skip to content
    Style: Wireframe

    Components · Feedback

    Announcement bar

    One line at the top of the site on an inverted ground, a read-more link and a close button.

    3.9% of the 2,073 sites in The UI Index have a alert (81 sites).

    Both previews render the same markup; the HTML tab below is that markup without a framework.

    Install

    React
    npx shadcn@latest add https://www.themes.dev/ui/r/announcement.json
    Astro
    npx shadcn@latest add https://www.themes.dev/ui/r/astro/announcement.json

    Or copy the files below into your project. They need clsx, tailwind-merge, and the themes.dev UI theme. The HTML needs only the compiled CSS and the script.

    announcement.client.ts
    // Announcement bar: the close button hides the bar; with data-remember it stays closed for the session
    // (sessionStorage, by the bar's id) while you browse; focus moves to the page's main landmark (or the body).
    declare global {
      interface Window {
        __caseroomAnnouncement?: boolean;
      }
    }
    
    if (typeof window !== 'undefined' && !window.__caseroomAnnouncement) {
      window.__caseroomAnnouncement = true;
      const restore = () => {
        for (const bar of document.querySelectorAll<HTMLElement>('[data-announcement]'))
          if (bar.id && bar.hasAttribute('data-remember') && sessionStorage.getItem(`caseroom-announcement:${bar.id}`) === 'closed') bar.hidden = true;
      };
      if (document.readyState === 'loading') document.addEventListener('DOMContentLoaded', restore);
      else restore();
      document.addEventListener('click', (e) => {
        const b = (e.target as Element).closest<HTMLButtonElement>('[data-announcement-close]');
        const bar = b?.closest<HTMLElement>('[data-announcement]');
        if (!bar) return;
        bar.hidden = true;
        if (bar.id && bar.hasAttribute('data-remember')) sessionStorage.setItem(`caseroom-announcement:${bar.id}`, 'closed');
        const main = document.querySelector<HTMLElement>('main');
        if (main) {
          if (!main.hasAttribute('tabindex')) main.tabIndex = -1;
          main.focus({ preventScroll: true });
        }
      });
    }
    
    export {};
    announcement.tsx
    import { cn } from '@/lib/utils';
    import './announcement.client';
    import { announcement as c, ARROW_PATH, CLOSE_PATH } from './announcement.variants';
    
    export interface AnnouncementProps {
      id: string;
      /** Keep it closed for the session once closed (remembered under the id). */
      remember?: boolean;
      text: string;
      link?: { label: string; href: string };
      className?: string;
    }
    
    /** One line at the top of the site, a read-more link, and a close button remembered for the session. */
    export function Announcement({ id, text, link, remember, className }: AnnouncementProps) {
      return (
        <aside id={id} data-announcement="" data-remember={remember ? '' : undefined} aria-label="Announcement" className={cn(c.root, className)}>
          <div className={c.inner}>
            <p className={c.text}>{text}</p>
            {link && <a href={link.href} className={c.link}>{link.label}<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true"><path d={ARROW_PATH} /></svg></a>}
          </div>
          <button type="button" data-announcement-close="" aria-label="Close the announcement" className={c.close}>
            <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" strokeWidth="1.5" strokeLinecap="round" aria-hidden="true"><path d={CLOSE_PATH} /></svg>
          </button>
        </aside>
      );
    }
    announcement.variants.ts
    /** An announcement bar (the most common alert in The UI Index): one line at the very top, a link to read more, and
     *  a close button that hides it and remembers that for the session. On an inverted ground, so it reads as the
     *  site's voice and not as page content. Centred: it is one line. */
    export const announcement = {
      root: 'inverted relative bg-background text-foreground',
      inner: 'mx-auto flex min-h-10 max-w-6xl items-center justify-center gap-x-3 gap-y-1 px-12 py-2 text-center text-sm max-sm:flex-wrap',
      text: 'text-pretty',
      link: 'inline-flex min-h-6 items-center gap-1 rounded-sm font-medium underline decoration-foreground/40 underline-offset-4 transition-colors hover:decoration-foreground active:bg-accent [&_svg]:size-3.5',
      close: 'absolute top-1/2 right-2 inline-flex size-8 -translate-y-1/2 cursor-pointer items-center justify-center rounded-md text-muted-foreground transition-colors hover:bg-accent hover:text-foreground active:bg-pressed [&_svg]:size-4',
    };
    export const ARROW_PATH = 'M3 8h10M9 4l4 4-4 4';
    export const CLOSE_PATH = 'M4 4l8 8M12 4l-8 8';