# Notification bell

> A header button with the unread count, which updates when a notification arrives or is read.

Source: https://bettersupabase.com/docs/registry/ui-recipes/notification-bell

{/* Generated by scripts/build-recipes.ts from the example apps. Edit the examples or the script, then run `pnpm recipes`. */}

Both sides call `useNotifications` from `better-supabase/blocks/notifications/react`; the [notifications block](https://bettersupabase.com/blocks/notifications) must be installed (`npx better-supabase add notifications`). Its page on bettersupabase.com is [/recipes/notification-bell](https://bettersupabase.com/recipes/notification-bell).

Web, with shadcn components:

```bash
npx shadcn add badge button
```

```bash
npx shadcn add @better-supabase/notifications-actions-next
```

```tsx title="apps/examples/nextjs/src/features/notifications/components/unread-badge.tsx"
"use client";

import type { NotificationCounts } from "better-supabase/blocks/notifications";

import { useNotifications } from "better-supabase/blocks/notifications/react";
import { BellIcon } from "lucide-react";
import { useExtracted } from "next-intl";

import { Badge } from "@/components/ui/badge";
import { buttonVariants } from "@/components/ui/button";
import { Link } from "@/i18n/navigation";
import { useAuth } from "@/lib/hooks";

import { loadNotificationCounts } from "../notifications-actions";

async function loadCounts(): Promise<readonly NotificationCounts[]> {
  const result = await loadNotificationCounts({});

  return result.ok ? [result.data] : [];
}

const unreadOf = (counts: readonly NotificationCounts[]): number =>
  counts[0]?.unread ?? 0;

/**
 * The unread count from the block's `counts()`, loaded again after each
 * broadcast on the caller's private topic `notifications:<user id>`.
 */
function useUnreadCount() {
  const auth = useAuth();
  const userId = auth.user?.id;

  const { items, count, status } = useNotifications({
    topic: userId ? `notifications:${userId}` : null,
    load: loadCounts,
    count: unreadOf,
  });

  return { count: items === undefined ? undefined : count, status };
}

/**
 * The header badge. Part of the static shell, so it loads from the browser
 * instead of adding a database call to every page.
 */
export function UnreadBadge() {
  const t = useExtracted("notifications");
  const { count, status } = useUnreadCount();

  return (
    <Link
      href="/notifications"
      className={buttonVariants({ variant: "ghost", size: "sm" })}
      data-testid="unread-badge"
      data-status={status}
      aria-label={t("Notifications, {count, number} unread", {
        count: count ?? 0,
      })}
    >
      <BellIcon />
      <Badge
        variant={count ? "default" : "secondary"}
        className="tabular-nums"
        data-testid="unread-count"
      >
        {count ?? "–"}
      </Badge>
    </Link>
  );
}
```

React Native, with `Pressable`, `Text`, `View` from `react-native`:

```bash
npx shadcn add @better-supabase/blocks
```

```tsx title="apps/examples/expo-powersync/src/components/notification-bell.tsx"
import type { NotificationCounts } from "better-supabase/blocks/notifications";

import { useNotifications } from "better-supabase/blocks/notifications/react";
import { tenantOf, useAuth, useSupabase } from "better-supabase/react";
import { Pressable, Text, View } from "react-native";

import { blocks } from "../lib/blocks";

const unreadOf = (counts: readonly NotificationCounts[]): number =>
  counts[0]?.unread ?? 0;

/**
 * The unread count, loaded again after each broadcast on `notifications:<user
 * id>`.
 */
export function NotificationBell({
  onPress,
}: {
  readonly onPress: () => void;
}) {
  const auth = useAuth();
  const supabase = useSupabase();
  const tenant = tenantOf(auth);
  const userId = auth.user?.id;

  const { items, count } = useNotifications({
    topic: userId ? `notifications:${userId}` : null,
    load: async () => {
      if (!tenant) {
        return [];
      }

      const result = await blocks(supabase).notifications.counts({ tenant });

      return result.ok ? [result.data] : [];
    },
    count: unreadOf,
  });

  return (
    <Pressable
      onPress={onPress}
      accessibilityRole="button"
      accessibilityLabel={`Notifications, ${String(count)} unread`}
      style={{ flexDirection: "row", alignItems: "center", gap: 8, padding: 8 }}
    >
      <Text>Notifications</Text>
      <View
        style={{
          minWidth: 24,
          borderRadius: 12,
          paddingHorizontal: 8,
          backgroundColor: count ? "#111" : "#e5e5e5",
        }}
      >
        <Text style={{ color: count ? "#fff" : "#111", textAlign: "center" }}>
          {items === undefined ? "–" : count}
        </Text>
      </View>
    </Pressable>
  );
}
```