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 newToday
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.