# Notification Banner

> iOS-style React notification banners for messages, orders and reminders that drop from the top one at a time, with a queue.

- Type: React component (Mobile)
- 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 notification-banner`
- Docs: https://wingo-ui.com/components/notification-banner

## Interaction

Mount the host once and call notify() from anywhere: a banner drops in from the top edge with a soft spring (the phone ticks once if the page has seen a tap) and leaves on its own after the duration. It shows the app icon, name and time ("acum", "acum 2 min."), the sender's photo or an icon tile, a one-line title and two lines of text. Tap or click it to open (onPress runs and the banner goes). Swipe it up with a finger, or drag it up with the mouse, to throw it away; a short drag springs back. Pull it down and, past 40px of rubber-banded pull, the grabber arms, the phone ticks and the card grows to the full text and its actions (44px buttons on touch); with a mouse, the chevron under the text does the same. The × appears on hover and on keyboard focus (touch uses the swipe). Timers pause while you hover, hold, focus or expand a banner and while the tab is hidden. More banners wait in a queue: the next one peeks out under the card with a "+2" chip and drops in after the current one leaves; a banner sent again with the same id updates in place and cross-fades its text. Keyboard: the hotkey (Alt+N, ⌥N on a Mac) focuses the banner without stealing focus on arrival; Enter opens it, ArrowDown expands and ArrowUp collapses, Tab reaches the actions, the chevron and the ×, and Escape dismisses it and returns focus to where you were. Each new banner is read once by screen readers. Below a 576px wide host banners span the top with 8px gutters under the notch; wider, they are a 380px card at the top end.

## Installation

```bash
npx wingo-ui@latest add notification-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/notification-banner` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/notification-banner.tsx`

npm dependencies: `npm i motion lucide-react`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [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))
- [Relative time](https://wingo-ui.com/components/relative-time) ([Markdown](https://wingo-ui.com/components/relative-time.md))
- [useLocale](https://wingo-ui.com/components/use-locale) ([Markdown](https://wingo-ui.com/components/use-locale.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.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 { useEffect, type ReactNode } from "react"
import { ShoppingCart } from "lucide-react"
import { NotificationBanner, notify } from "@/components/ui/notification-banner"

const usd = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD", maximumFractionDigits: 0 })

export function CrmShell({ children }: { children: ReactNode }) {
  return (
    <>
      {children}
      <NotificationBanner
        appName="Northwind CRM"
        labels={{ queued: (n) => `+${n} waiting` }}
      />
    </>
  )
}

export function useNewOrders(orders: { id: number; total: number; locker: string }[], openOrder: (id: number) => void) {
  useEffect(() => {
    const order = orders.at(-1)
    if (!order) return
    notify({
      id: `order-${order.id}`,
      title: "New order",
      message: `Order #${order.id} · ${usd.format(order.total)} · ${order.locker}`,
      icon: <ShoppingCart />,
      tone: "success",
      onPress: () => openOrder(order.id),
      actions: [{ label: "View order", onSelect: () => openOrder(order.id) }],
    })
  }, [orders, openOrder])
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `position?` | `"top-center" \| "top-right" \| "top-left"` | `"top-right"` | Where the card sits once the host is at least 576px wide (a container query, so a phone frame on a desktop still gets the phone layout). Narrower, banners always span the top with 8px gutters. |
| `strategy?` | `"fixed" \| "absolute"` | `"fixed"` | fixed pins the host to the viewport (the app default). absolute places it inside a relative container: a phone frame, an embedded panel. |
| `width?` | `number` | `380` | Card width in px in the wide layout. Phones always use the full width minus the gutters. |
| `offset?` | `number` | `8` | px below the top safe area. In the wide layout it is also the distance from the side. |
| `safeArea?` | `boolean` | `true` | Pads env(safe-area-inset-top) so banners drop in under the notch or the Dynamic Island. It is 0 in desktop browsers and iframes. |
| `variant?` | `"glass" \| "solid"` | `"glass"` | glass: a translucent popover with a strong backdrop blur, like iOS (solid where backdrop-filter is missing). solid: the plain popover surface. |
| `radius?` | `"sm" \| "md" \| "lg"` | `"lg"` | Card corners: sm 12px, md 16px, lg 24px (iOS). The action buttons and the media tile follow. |
| `showAppHeader?` | `boolean` | `true` | The row with the app icon, the app name and the time. Off, the time moves next to the title (a compact chat banner). |
| `appName?` | `string` | `""` | The app name in the header of every banner (a banner's own appName wins). The app icon shows its first letter on the tone's fill; empty shows a bell. |
| `appIcon?` | `ReactNode` |  | Replaces the letter tile in the header (a 20px logo). A banner's own appIcon wins. |
| `duration?` | `number` | `5000` | ms a banner stays before it leaves. The clock pauses while it is hovered, held, focused or expanded. A banner's own duration or sticky wins. |
| `expandable?` | `boolean` | `true` | Pull down, the chevron, a tap on a banner without onPress, or ArrowDown show the full text and the actions. Off, the actions never show. |
| `maxQueue?` | `number` | `5` | How many banners may wait behind the one on screen. Past it the oldest queued banner is dropped (its onDismiss runs with "dropped"). |
| `haptics?` | `boolean` | `true` | A light tick when a banner arrives (only once the page has seen a tap) and a selection tick when it expands. Through lib/haptics. |
| `hotkey?` | `string` | `"alt+n"` | Moves focus to the banner on screen; Escape then dismisses it and returns focus. Empty turns it off. |
| `pauseWhenPageIsHidden?` | `boolean` | `true` | Timers stop while the tab is in the background, so a banner sent then is still there when you come back. |
| `hostId?` | `string` | `""` | Shows only banners sent with notify({ hostId }). Several independent hosts on one page (a phone preview next to the app). A second host with the same id stays empty. |
| `locale?` | `string` | `""` | Formats the time ("2 min ago", "il y a 2 min") through relative-time. Empty uses the UIProvider locale, then the page language. |
| `labels?` | `Partial<NotificationBannerLabels>` |  | Every string: region "Notifications", dismiss "Dismiss", expand "Show more", collapse "Show less", queued(n) "{n} more", now "now", hint "Swipe up to dismiss, pull down for more" (read with each banner). DEFAULT_NOTIFICATION_BANNER_LABELS has the English set. |
| `classNames?` | `Partial<Record<"item" \| "header" \| "body" \| "media" \| "title" \| "message" \| "actions" \| "action" \| "close" \| "expand", string>>` |  | Extra classes for the parts of every banner. |
| `className?` | `string` |  | Extra classes for the host section (the full-width layer at the top). |

## Parts

### notify (function)

Shows a banner and returns its id. Works from anywhere (handlers, fetch helpers, outside React) and before the host mounts; ignored on the server. Options: { id?, hostId?, title, message?, icon?, avatar?: { src?, name }, appName?, appIcon?, time? (default now), tone?, color?, onPress?, actions?: { label, onSelect, tone?: "neutral" | "danger" }[], duration?, sticky?, defaultExpanded?, onDismiss?(reason) }. The same id updates the banner in place. Signature: (options: NotificationBannerOptions) => string. notify.update (id: string, options: Partial<NotificationBannerOptions>) => boolean: Changes a shown or queued banner in place (upload 2/3, a new count) and restarts its clock. false when the id is gone. notify.dismiss (id?: string) => void: Removes one banner, or every banner (shown and queued) without an id. onDismiss runs with "api".

### useNotificationBanners (hook)

The banner on screen and the ones waiting, for a counter or a "clear all" button. Signature: (hostId?: string) => { current: NotificationBannerRecord | null; queue: NotificationBannerRecord[] }.

### NotificationBannerCard (component)

One banner as a still surface, without the store, timers or gestures: the same options plus variant, radius, showAppHeader, expanded / defaultExpanded / onExpandedChange, onClose, queued, locale, labels and classNames. For previews and settings screens. Props: NotificationBannerCardProps.

### DEFAULT_NOTIFICATION_BANNER_LABELS (constant)

The English strings: region, dismiss, expand, collapse, the queued count, now and the swipe hint. Spread it and override what you translate.

## Examples

- **A queue of three**: Three calls in a row: the first drops in, the next peeks out under it with a +2 chip. In a host 576px or wider it is a card at the top end.
- **Compact chat banner**: Solid surface, no app row: the time sits next to the sender, like a messenger.
- **Sticky, needs an answer**: A new version banner that arrives expanded and stays until you reload, postpone or swipe it away.
- **Updating in place**: The same id sent again: 1/3, 2/3, 3/3, then a success banner, without a new drop.
- **Brand tone for the marketplace**: Northwind Market colors its app icon with the brand orange; the buyer's photo stays the media.
- **British locale**: locale="en-GB" with the default labels: a banner sent a few minutes ago reads "3 min ago" and times use 24 hours.

## See also

- [Toast](https://wingo-ui.com/components/toast) ([Markdown](https://wingo-ui.com/components/toast.md))
- [Notification Center](https://wingo-ui.com/components/notification-center) ([Markdown](https://wingo-ui.com/components/notification-center.md))
- [Notification Bell](https://wingo-ui.com/components/notification-bell) ([Markdown](https://wingo-ui.com/components/notification-bell.md))
- [Install Prompt](https://wingo-ui.com/components/install-prompt) ([Markdown](https://wingo-ui.com/components/install-prompt.md))
- [Haptics](https://wingo-ui.com/components/haptics) ([Markdown](https://wingo-ui.com/components/haptics.md))

---

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