Skip to content
All Taste components

Communication

Notifications panel

A notifications list grouped into Today and Earlier, with unread markers and an inline invite.

unread dots · Today/Earlier groups · filter tabs · inline actions · mark all read

Notifications

3 new
Today
PN
Priya Nair mentioned you in Diwali menu: “Can we add the kaju katli box?”
Payment of ₹12,400 received from Kaveri Foods
Rohan Das invited you to Coastal Kitchens
Earlier
SK
Sana Khan approved your leave for 2–3 Nov
Your GST return for September is due in 5 days
"use client";
import { useState } from "react";
import { Check, CreditCard, Users } from "lucide-react";
import { cn } from "@/lib/utils";

export interface Notice {
  id: string;
  group: "Today" | "Earlier";
  kind: "mention" | "payment" | "invite";
  initials?: string;
  text: React.ReactNode;
  time: string;
  unread?: boolean;
}

const sample: Notice[] = [
  { id: "1", group: "Today", kind: "mention", initials: "PN", unread: true, time: "8 min ago",
    text: <><b className="font-medium">Priya Nair</b> mentioned you in <b className="font-medium">Diwali menu</b>: “Can we add the kaju katli box?”</> },
  { id: "2", group: "Today", kind: "payment", unread: true, time: "1 h ago",
    text: <>Payment of <b className="font-medium tabular-nums">₹12,400</b> received from Kaveri Foods</> },
  { id: "3", group: "Today", kind: "invite", unread: true, time: "3 h ago",
    text: <><b className="font-medium">Rohan Das</b> invited you to <b className="font-medium">Coastal Kitchens</b></> },
  { id: "4", group: "Earlier", kind: "mention", initials: "SK", time: "Mon, 12 Oct",
    text: <><b className="font-medium">Sana Khan</b> approved your leave for 2–3 Nov</> },
];

const tabs = [["all", "All"], ["mention", "Mentions"], ["payment", "Payments"]] as const;

export function NotificationsPanel({ items = sample, onAccept, onDecline }:
  { items?: Notice[]; onAccept?: (id: string) => void; onDecline?: (id: string) => void }) {
  const [list, setList] = useState(items);
  const [tab, setTab] = useState<(typeof tabs)[number][0]>("all");
  const shown = list.filter((n) => tab === "all" || n.kind === tab);
  const unread = list.filter((n) => n.unread).length;

  return (
    <section className="w-full max-w-[400px] overflow-hidden rounded-lg border border-border bg-bg" aria-label="Notifications">
      <div className="px-4 pt-3.5">
        <div className="flex items-center justify-between gap-3">
          <div className="flex items-center gap-2">
            <h3 className="text-sm font-medium">Notifications</h3>
            {unread > 0 && <span className="rounded-full bg-primary-soft px-2 py-0.5 text-xs font-medium text-primary-soft-fg">{unread} new</span>}
          </div>
          <button onClick={() => setList((l) => l.map((n) => ({ ...n, unread: false })))}
            className="inline-flex h-8 items-center gap-1.5 rounded px-2.5 text-[13px] font-medium hover:bg-surface-2">
            <Check className="size-3.5" aria-hidden /> Mark all read
          </button>
        </div>
        <div role="tablist" className="mt-2 flex gap-[18px] border-b border-border">
          {tabs.map(([key, label]) => (
            <button key={key} role="tab" aria-selected={tab === key} onClick={() => setTab(key)}
              className="-mb-px h-9 border-b-2 border-transparent text-[13px] font-medium text-fg-muted aria-selected:border-primary aria-selected:text-fg">
              {label}
            </button>
          ))}
        </div>
      </div>

      {(["Today", "Earlier"] as const).map((group) => {
        const rows = shown.filter((n) => n.group === group);
        if (!rows.length) return null;
        return (
          <div key={group}>
            <p className="px-4 pb-1 pt-3 text-xs font-medium text-fg-subtle">{group}</p>
            {rows.map((n) => (
              <div key={n.id} className={cn("grid grid-cols-[8px_28px_minmax(0,1fr)] gap-x-2.5 py-2.5 pl-2.5 pr-4 text-[13.5px]", n.unread && "bg-surface")}>
                <span className={cn("mt-[11px] size-1.5 rounded-full", n.unread && "bg-primary")} aria-label={n.unread ? "Unread" : undefined} />
                {n.initials ? (
                  <span className="grid size-7 place-items-center rounded-full bg-primary-soft text-[11px] font-medium text-primary-soft-fg">{n.initials}</span>
                ) : (
                  <span className={cn("grid size-7 place-items-center rounded-full", n.kind === "payment" ? "bg-success-soft text-success-fg" : "bg-surface-2 text-fg-muted")}>
                    {n.kind === "payment" ? <CreditCard className="size-3.5" aria-hidden /> : <Users className="size-3.5" aria-hidden />}
                  </span>
                )}
                <div>
                  <p>{n.text}</p>
                  <time className="mt-0.5 block text-xs text-fg-subtle">{n.time}</time>
                  {n.kind === "invite" && (
                    <div className="mt-2 flex gap-2">
                      <button onClick={() => onAccept?.(n.id)} className="h-[30px] rounded bg-primary px-2.5 text-[13px] font-medium text-primary-fg hover:bg-primary-hover">Accept</button>
                      <button onClick={() => onDecline?.(n.id)} className="h-[30px] rounded border border-border-strong px-2.5 text-[13px] font-medium hover:bg-surface">Decline</button>
                    </div>
                  )}
                </div>
              </div>
            ))}
          </div>
        );
      })}

      <div className="border-t border-border px-4 py-2.5 text-center">
        <a href="/settings/notifications" className="text-[12.5px] text-primary hover:underline">Notification settings</a>
      </div>
    </section>
  );
}

The code uses the Taste Tailwind preset. Make yours on the Taste page with Export my system.