# Banner

> A React announcement banner: a full-width strip for the top of an app, page or panel, with tones, one action and a dismissal it remembers.

- Type: React component (Feedback)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add banner`
- Docs: https://wingo-ui.com/components/banner

## Interaction

The strip says one thing to everyone: an icon, a bold title and the rest of the sentence on the same line, with one action at the end. Press the action and it shows a spinner while its promise runs; with only a link it is an underlined link with an arrow. The close button (Tab to it, then Enter or Space) collapses the strip, the page below slides up, and focus moves to the next control instead of getting lost; with persistKey the dismissal is remembered in local storage and the banner stays away, forever or for dismissDays days. Sticky banners stay pinned at the top of their scroller and fixed ones at the top of the viewport, below the notch, writing their height to --banner-offset so a topbar can move down. Banners that appear after the page loaded slide down from the top edge; with reduced motion they only fade. On a phone (below 640px) the banner turns compact: the description hides, the text keeps to two lines, the action becomes a chevron and the whole 44px row is the tap target, while the close button keeps its own 44px hit area. With announce on, screen readers hear the title politely when it appears, which suits offline notices.

## Installation

```bash
npx wingo-ui@latest add banner
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/banner` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/banner.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Button](https://wingo-ui.com/components/button) ([Markdown](https://wingo-ui.com/components/button.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Tones](https://wingo-ui.com/components/tones) ([Markdown](https://wingo-ui.com/components/tones.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
import { Banner } from "@/components/ui/banner"

export function ReleaseBanner() {
  return (
    <Banner
      title="Version 3.2 is here"
      description="New invoicing reports and Excel export."
      action={{ label: "See what's new", href: "/whats-new" }}
      persistKey="banner-v32"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `title?` | `ReactNode` | `""` | The bold lead of the sentence. The playground shows a sample when it is empty. |
| `description?` | `ReactNode` | `""` | The rest of the sentence, on the same line after a middot. Hidden in compact mode, so keep the title self-sufficient. |
| `actionLabel?` | `string` | `""` | Shortcut for action.label: a small button at the end. The playground wires it to a fake 1.2s request. |
| `action?` | `{ label: ReactNode; onClick?: () => void \| Promise<unknown>; href?: string; target?: string }` |  | The one action. A promise from onClick shows the loading state until it settles. With only href it renders as an underlined link with an arrow. In compact mode the whole row presses it. |
| `children?` | `ReactNode` |  | Free content instead of (or after) the title and description: a countdown, a progress bar. |
| `icon?` | `ReactNode \| false` |  | The leading icon. Defaults per tone: neutral Megaphone, info Info, success CircleCheck, warning TriangleAlert, danger CircleAlert, brand Sparkles. false hides it. |
| `tone?` | `"neutral" \| "brand" \| "success" \| "warning" \| "danger" \| "info"` | `"neutral"` | Semantic color. Neutral solid is a black strip in light mode and a white one in dark mode. |
| `color?` | `string` | `""` | Any CSS color. Replaces the tone; text on a solid strip turns black or white by lightness. |
| `variant?` | `"solid" \| "soft" \| "outline"` | `"solid"` | solid for app-wide news, soft for a tinted strip (warnings, trials), outline for a hairline strip inside cards and panels. |
| `size?` | `"sm" \| "md"` | `"md"` | sm is at least 36px tall with 13px text, md 44px with 14px text. |
| `defaultOpen?` | `boolean` | `true` | Starts visible. Use open and onOpenChange to control it; open wins over persistKey and nothing is stored then. |
| `open?` | `boolean` |  | Controlled visibility. onOpenChange(false) fires on dismiss. |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the close button is pressed (also with persistKey). |
| `onDismiss?` | `() => void` |  | Fires after the collapse animation finished. |
| `dismissible?` | `boolean` | `true` | Shows the close button, with a 44px hit area on touch screens. |
| `persistKey?` | `string` | `""` | Remembers the dismissal ({ at: ISO date }) in local storage under this key. Nothing renders until storage is read, so a dismissed banner never flashes. |
| `dismissDays?` | `number` | `0` | With persistKey: 0 keeps the banner dismissed forever, otherwise it returns after this many days. |
| `position?` | `"static" \| "sticky" \| "fixed"` | `"static"` | sticky pins it to the top of the nearest scroller, below var(--topbar-offset). fixed pins it to the viewport top with the safe area and writes its height to --banner-offset on <html> (0 while hidden). One fixed banner at a time; stack several as static banners in one fixed wrapper. |
| `align?` | `"center" \| "start"` | `"center"` | Where the content row sits. Centered text stays centered even with a close button at the end. |
| `contentWidth?` | `"full" \| "page"` | `"page"` | page caps the content row at max-w-6xl like a page; full uses the whole width (panels, cards). |
| `compact?` | `"auto" \| "always" \| "never"` | `"auto"` | auto goes compact below 640px: the description hides, the text keeps to two lines, the action becomes a chevron and the whole row is its tap target. |
| `href?` | `string` | `""` | Makes the whole banner a link. The close button and the action stay separate. |
| `announce?` | `boolean` | `false` | Reads the title in a polite live region when the banner appears after mount. For offline style notices. |
| `safeArea?` | `boolean` | `true` | A fixed banner pads env(safe-area-inset-top), so it clears the notch. |
| `labels?` | `Partial<{ region: string; dismiss: string }>` |  | The region's aria-label ("Announcement") and the close button's aria-label ("Dismiss"). DEFAULT_BANNER_LABELS has the English set. |
| `classNames?` | `Partial<Record<"inner" \| "body" \| "icon" \| "content" \| "title" \| "description" \| "action" \| "close", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the strip itself, merged last. |

## Parts

### DEFAULT_BANNER_LABELS (constant)

The English labels: region ("Announcement", the landmark's aria-label) and dismiss ("Dismiss", the close button). Type: BannerLabels.

### BannerAction (type)

The action prop: { label, onClick?, href?, target? }. A returned promise keeps the action loading until it settles; with only href it is an underlined link with an arrow.

### BannerDismissal (type)

What persistKey stores in local storage: { at }, the ISO time of the dismissal, read against dismissDays.

## Examples

- **Variants and tones**: solid, soft and outline in the six tones. Neutral solid is black in light mode and white in dark.
- **Maintenance**: A warning soft strip that cannot be dismissed, with a custom icon.
- **Trial ending**: Brand belongs in examples. The action's promise shows a spinner, then the copy changes.
- **Offline**: Controlled by the browser's online and offline events; announce reads it to screen readers.
- **Sale with an end date**: A custom color, a live countdown in the description and a link action.
- **Inside a card**: outline, sm and full width over a list. Sending the invoices collapses the strip.
- **Sticky and remembered**: Sticks to the top of the scroller; persistKey keeps it closed after a reload.

## See also

- [Alert](https://wingo-ui.com/components/alert) ([Markdown](https://wingo-ui.com/components/alert.md))
- [Notification Banner](https://wingo-ui.com/components/notification-banner) ([Markdown](https://wingo-ui.com/components/notification-banner.md))
- [Cookie Consent](https://wingo-ui.com/components/cookie-consent) ([Markdown](https://wingo-ui.com/components/cookie-consent.md))
- [Sticky CTA Bar](https://wingo-ui.com/components/sticky-cta-bar) ([Markdown](https://wingo-ui.com/components/sticky-cta-bar.md))
- [Countdown](https://wingo-ui.com/components/countdown) ([Markdown](https://wingo-ui.com/components/countdown.md))
- [usePersistedState](https://wingo-ui.com/components/use-persisted-state) ([Markdown](https://wingo-ui.com/components/use-persisted-state.md))

---

Canonical: https://wingo-ui.com/components/banner
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
