# Reactions bar

> Emoji reactions under a record with counts, which toggle the caller's own reaction.

Source: https://bettersupabase.com/docs/registry/ui-recipes/reactions-bar

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

It loads its data on the server through the block's client; the [reactions block](https://bettersupabase.com/blocks/reactions) must be installed (`npx better-supabase add reactions`). Its page on bettersupabase.com is [/recipes/reactions-bar](https://bettersupabase.com/recipes/reactions-bar).

Web, with shadcn components:

```bash
npx shadcn add alert button card skeleton
```

```tsx title="apps/examples/nextjs/src/features/customers/components/reactions-bar.tsx"
const EMOJIS = ["👍", "🎉", "❤️", "👀", "🚀"] as const;

/**
 * Quick reactions on the customer. The counts load in the browser and update
 * when anyone in the organization reacts (the `reactions` topic).
 */
export function ReactionsBar({
  customerId,
  canReact,
}: {
  customerId: string;
  canReact: boolean;
}) {
  const t = useExtracted("customers");
  const [busy, setBusy] = useState<string | null>(null);

  const subject = { type: "customer", id: customerId };

  const { reactions, error, toggle, refresh } = useReactions({
    subjects: [subject],
  });

  const counts = new Map(
    (reactions?.[subjectKey(subject)] ?? []).map((each) => [each.emoji, each]),
  );

  const total = [...counts.values()].reduce((sum, each) => sum + each.count, 0);

  return (
    <Card size="sm" data-recipe="reactions-bar">
      <CardHeader>
        <CardTitle>{t("Reactions")}</CardTitle>
      </CardHeader>
      <CardContent className="flex flex-col gap-3">
        {error ? (
          <Alert variant="destructive">
            <TriangleAlertIcon />
            <AlertTitle>{t("The reactions didn't load")}</AlertTitle>
            <AlertDescription>{t("Try again in a moment.")}</AlertDescription>
            <AlertAction>
              <Button
                size="sm"
                variant="outline"
                onClick={() => {
                  void refresh();
                }}
              >
                {t("Retry")}
              </Button>
            </AlertAction>
          </Alert>
        ) : null}
        {reactions === undefined ? (
          error ? null : (
            <div className="flex gap-2">
              {EMOJIS.map((emoji) => (
                <Skeleton key={emoji} className="h-8 w-12" />
              ))}
            </div>
          )
        ) : (
          <div className="flex flex-col gap-2">
            <fieldset className="flex flex-wrap gap-2">
              <legend className="sr-only">{t("React to this customer")}</legend>
              {EMOJIS.map((emoji) => {
                const count = counts.get(emoji);

                return (
                  <Button
                    key={emoji}
                    size="sm"
                    variant={count?.reacted ? "secondary" : "outline"}
                    aria-pressed={count?.reacted ?? false}
                    disabled={!canReact || busy !== null}
                    onClick={() => {
                      setBusy(emoji);
                      void toggle(subject, emoji).finally(() => {
                        setBusy(null);
                      });
                    }}
                  >
                    <span aria-hidden>{emoji}</span>
                    <span className="tabular-nums">{count?.count ?? 0}</span>
                  </Button>
                );
              })}
            </fieldset>
            {total === 0 ? (
              <p className="text-xs text-muted-foreground">
                {canReact
                  ? t("No reactions yet. Pick one to be the first.")
                  : t("No reactions yet.")}
              </p>
            ) : null}
          </div>
        )}
      </CardContent>
    </Card>
  );
}
```