# Announcement bar (Caseroom component)

One line at the top of the site on an inverted ground, a read-more link and a close button. Black and white, dark mode, shadcn-compatible; React, Astro and plain HTML.

3.8% of the 2,062 sites in The UI Index have a alert (78 sites).

## Install

- React: `npx shadcn@latest add https://www.themes.dev/caseroom/r/announcement.json`
- Astro: `npx shadcn@latest add https://www.themes.dev/caseroom/r/astro/announcement.json`
- HTML: https://www.themes.dev/caseroom/r/html/announcement.html (with https://www.themes.dev/caseroom/r/html/caseroom.css)

Page: https://www.themes.dev/caseroom/components/announcement/ · Rules for agents: https://www.themes.dev/caseroom/AGENT.md

## Code

### announcement.astro

```astro
---
import { cn } from '@/lib/utils';
import { announcement as c, ARROW_PATH, CLOSE_PATH } from './announcement.variants';

interface Props {
  id: string;
  /** Keep it closed for the session once closed (remembered under the id). */
  remember?: boolean;
  text: string;
  link?: { label: string; href: string };
  class?: string;
}
const { id, text, link, remember, class: className } = Astro.props;
---

<aside id={id} data-announcement data-remember={remember ? '' : undefined} aria-label="Announcement" class={cn(c.root, className)}>
  <div class={c.inner}>
    <p class={c.text}>{text}</p>
    {link && <a href={link.href} class={c.link}>{link.label}<svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d={ARROW_PATH} /></svg></a>}
  </div>
  <button type="button" data-announcement-close aria-label="Close the announcement" class={c.close}>
    <svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" aria-hidden="true"><path d={CLOSE_PATH} /></svg>
  </button>
</aside>

<script>
  import './announcement.client';
</script>
```

### announcement.client.ts

```tsx
// 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

```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

```tsx
/** 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';
```
