# Notification

> Notification items and a ready-made notification center with unread state and mark-as-read.

Category: Feedback · Status: stable · Package: @bindshq/stance-react@0.1.1 · Docs: https://stanceui.com/components/notification

Notification renders one event: who did what and when, with an optional avatar or icon, actions and unread state. NotificationList groups them, and NotificationCenter adds a header with an unread count, “Mark all as read”, an empty state and a footer. Place the center in a Popover behind a bell button or in a Drawer on phones.

## Import

```tsx
import { Notification, NotificationList, NotificationCenter } from '@bindshq/stance-react';
```

Styles are imported once per app: `@bindshq/stance-tokens/css` and `@bindshq/stance-react/styles.css`.

## Examples

### In a bell popover

```tsx
import * as React from 'react';
import { Button, Icon, Notification, NotificationCenter, Popover, PopoverContent, PopoverTrigger } from '@bindshq/stance-react';
import type { IconName } from '@bindshq/stance-react';

interface Item {
  id: number;
  title: string;
  description?: string;
  minutes: number;
  unread: boolean;
  icon: IconName;
}

const initial: Item[] = [
  {
    id: 1,
    title: 'Ada Lovelace approved “Sales, EMEA”',
    description: 'It will deploy on the next sync.',
    minutes: 4,
    unread: true,
    icon: 'check-circle',
  },
  {
    id: 2,
    title: '12 new users synced from Azure AD',
    description: 'They received the default signature.',
    minutes: 95,
    unread: true,
    icon: 'users',
  },
  {
    id: 3,
    title: 'Exchange relay certificate renewed',
    minutes: 60 * 26,
    unread: false,
    icon: 'shield',
  },
];

function useItems() {
  const [items, setItems] = React.useState(initial.map((n) => ({ ...n })));
  const markRead = (id: number) =>
    setItems((all) => all.map((n) => (n.id === id ? { ...n, unread: false } : n)));
  const markAll = () => setItems((all) => all.map((n) => ({ ...n, unread: false })));
  return { items, markRead, markAll, unread: items.filter((n) => n.unread).length };
}

export function BellPopover() {
  const { items, markRead, markAll, unread } = useItems();
  return (
    <Popover>
      <PopoverTrigger asChild>
        <Button
          variant="outline"
          aria-label={`Notifications, ${unread} unread`}
          leadingIcon={<Icon name="bell" />}
        >
          {unread}
        </Button>
      </PopoverTrigger>
      <PopoverContent aria-label="Notifications" align="end" style={{ padding: 0, width: 380 }}>
        <NotificationCenter
          unreadCount={unread}
          onMarkAllRead={markAll}
          footer={<a href="#all">View all notifications</a>}
        >
          {items.map((n) => (
            <Notification
              key={n.id}
              title={n.title}
              description={n.description}
              time={new Date(Date.now() - n.minutes * 60_000)}
              unread={n.unread}
              onMarkRead={() => markRead(n.id)}
              media={<Icon name={n.icon} size={20} />}
            />
          ))}
        </NotificationCenter>
      </PopoverContent>
    </Popover>
  );
}
```

### List

```tsx
import { Button, Icon, Notification, NotificationList } from '@bindshq/stance-react';

export function List() {
  return (
    <NotificationList label="Recent activity" style={{ maxWidth: 480 }}>
      <Notification
        unread
        title="Grace Hopper requested access to “Brand 2026”"
        time="2 minutes ago"
        media={<Icon name="user" size={20} />}
        actions={
          <>
            <Button size="xs">Approve</Button>
            <Button size="xs" variant="outline">
              Decline
            </Button>
          </>
        }
      />
      <Notification
        title="Weekly deployment report is ready"
        href="#report"
        time="Yesterday"
        media={<Icon name="file-text" size={20} />}
      />
    </NotificationList>
  );
}
```

### Empty state

```tsx
import { NotificationCenter } from '@bindshq/stance-react';

export function Empty() {
  return (
    <div
      style={{ width: 360, border: '1px solid var(--st-color-border-default)', borderRadius: 12 }}
    >
      <NotificationCenter />
    </div>
  );
}
```

## API reference

| Component | Prop | Type | Default | Description |
| --- | --- | --- | --- | --- |
| Notification | `title` (required) | `ReactNode` | None | What happened. |
| Notification | `description` | `ReactNode` | None | Supporting detail. |
| Notification | `time` | `Date \| string` | None | Dates render as relative time in a <time> element. |
| Notification | `media` | `ReactNode` | None | Avatar, icon or logo. |
| Notification | `unread` | `boolean` | `false` | Shows a dot and announces “Unread”. |
| Notification | `href` | `string` | None | Makes the whole item a link to the related page. |
| Notification | `onMarkRead` | `() => void` | None | Shows a mark-as-read button while unread. |
| Notification | `actions` | `ReactNode` | None | Inline actions such as Approve / Decline. |
| Notification | `markReadLabel` | `string` | `'Mark as read'` | Button label. |
| NotificationList | `label` | `string` | `'Notifications'` | List name (ignored when aria-labelledby is set). |
| NotificationCenter | `title` | `ReactNode` | `'Notifications'` | Heading. |
| NotificationCenter | `unreadCount` | `number` | `0` | Shown beside the heading. |
| NotificationCenter | `onMarkAllRead` | `() => void` | None | Shows “Mark all as read” when there are unread items. |
| NotificationCenter | `emptyState` | `ReactNode` | `'You’re all caught up.'` | Shown with no children. |
| NotificationCenter | `footer` | `ReactNode` | None | E.g. a “View all” link. |

## Accessibility

- Items are list items; unread ones start with visually hidden “Unread:” text so the state is not conveyed by colour or weight alone.
- NotificationCenter is a labelled region whose heading announces the unread count.
- With href the whole row is clickable while inner buttons stay independently focusable.
- Announce new arrivals separately (e.g. update the bell button’s label); the list is not a live region.

## Guidelines

**Do**

- Lead with the actor and action: “Ada approved Sales, EMEA”.
- Link each notification to where the user can act.

**Don’t**

- Use notifications for confirmations of the user’s own actions; use Toast.

## Related

- https://stanceui.com/components/popover
- https://stanceui.com/components/toast
- https://stanceui.com/components/badge
