App screens
Notifications feed
A real notifications panel: All/Unread/Inbox Tabs filter the list, each row's content varies by kind (a reaction, a quoted comment with Reply, a downloadable PDF, a pair of image uploads, or an Accept/Deny join request), and a small dot marks anything unread.
notifications-feed.tsx
How it's built
1
The Unread tab filters the same array by a real `unread` field — there's no separate unread dataset to keep in sync.
2
Each notification `kind` renders its own extra content (a quote box, a file card, an image grid, or actions) below the shared avatar/text/timestamp row.
3
The status badge on the avatar (online dot, comment icon, upload dot, verified check) is purely decorative — screen readers get the same information from the sentence text.
Source
One file · copy & pasteimport { useState } from "react";
import { Avatar } from "oks-ui/avatar";
import { Button } from "oks-ui/button";
import { Dropdown, DropdownItem, DropdownMenu, DropdownTrigger } from "oks-ui/dropdown";
import { Tab, Tabs } from "oks-ui/tabs";
import { DownloadIcon, FileTextIcon, MessageCircleIcon, MoreIcon } from "../../showcase/icons";
// A checkmark inside a ring — "verified request".
function VerifiedIcon({ size = 11 }: { size?: number }) {
return (
<svg width={size} height={size} viewBox="0 0 16 16" fill="none" stroke="white" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<circle cx="8" cy="8" r="7" fill="var(--oks-color-info-500)" stroke="none" />
<path d="M5 8.2 7 10l4-4.5" />
</svg>
);
}
type Person = { name: string; photo?: string };
const ALBERT: Person = { name: "Albert Juan", photo: "/hero/person-man.jpg" };
const LAWRENCE: Person = { name: "Lawrence", photo: "/avatars/synthetic-1.jpg" };
const MARY: Person = { name: "Mary Cruz", photo: "/hero/person-woman.jpg" };
const ERIC: Person = { name: "Eric Young", photo: "/avatars/synthetic-2.jpg" };
type Notification =
| { id: number; kind: "reaction"; person: Person; target: string; time: string; unread: boolean }
| { id: number; kind: "comment"; person: Person; target: string; quote: string; time: string; unread: boolean }
| { id: number; kind: "upload"; person: Person; target: string; fileName: string; fileSize: string; time: string; unread: boolean }
| { id: number; kind: "images"; person: Person; target: string; covers: string[]; time: string; unread: boolean }
| { id: number; kind: "join-request"; person: Person; channel: string; time: string; unread: boolean };
const TODAY: Notification[] = [
{ id: 1, kind: "reaction", person: ALBERT, target: "#web-app post", time: "2 min ago", unread: true },
{ id: 2, kind: "comment", person: LAWRENCE, target: "#UXDesign group", quote: "Great design! Improve mobile responsiveness…", time: "Today, 1:30 pm", unread: true },
{ id: 3, kind: "upload", person: LAWRENCE, target: "AppDev Page", fileName: "UX-report.pdf", fileSize: "5mb", time: "2 min ago", unread: true },
{ id: 4, kind: "images", person: MARY, target: "Rebranding page", covers: ["linear-gradient(140deg, #1a2a52 0%, #5a3fae 60%, #e0623a 100%)", "linear-gradient(140deg, #171923 0%, #7a2d2d 45%, #e0623a 100%)"], time: "2 min ago", unread: true },
];
const PREV_WEEK: Notification[] = [
{ id: 5, kind: "join-request", person: ERIC, channel: "#marketing", time: "2 min ago", unread: false },
];
function NotificationRow({ n }: { n: Notification }) {
return (
<div className="flex gap-3 py-3">
<div className="relative shrink-0">
<Avatar src={n.person.photo} name={n.person.name} size="md" radius="full" />
{n.kind === "reaction" ? (
<span aria-hidden="true" className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full ring-2 ring-paper" style={{ background: "var(--oks-color-success-500)" }} />
) : n.kind === "comment" ? (
<span className="absolute -bottom-1 -right-1 flex h-4 w-4 items-center justify-center rounded-full bg-paper text-ink-muted ring-2 ring-paper">
<MessageCircleIcon size={10} />
</span>
) : n.kind === "upload" ? (
<span aria-hidden="true" className="absolute -bottom-0.5 -right-0.5 h-3 w-3 rounded-full ring-2 ring-paper" style={{ background: "var(--oks-color-warning-500)" }} />
) : n.kind === "join-request" ? (
<span className="absolute -bottom-1 -right-1 rounded-full ring-2 ring-paper">
<VerifiedIcon />
</span>
) : null}
</div>
<div className="min-w-0 flex-1">
<p className="text-[0.85rem] leading-snug text-ink-muted">
<span className="font-semibold text-ink">{n.person.name}</span>{" "}
{n.kind === "reaction" ? (
<>
Added reaction 🤔 <span className="font-medium text-ink">{n.target}</span>
</>
) : n.kind === "comment" ? (
<>
added a new comment in <span className="font-medium text-ink">{n.target}</span>
</>
) : n.kind === "upload" ? (
<>
Uploaded 2 attachments in <span className="font-medium text-ink">{n.target}</span>
</>
) : n.kind === "images" ? (
<>
Uploaded {n.covers.length} images to <span className="font-medium text-ink">{n.target}</span>
</>
) : (
<>
requested to join <span className="font-medium text-ink">{n.channel}</span>
</>
)}
</p>
<p className="text-[0.75rem] text-ink-faint">{n.time}</p>
{n.kind === "comment" ? (
<div className="mt-2 flex items-center justify-between gap-3 rounded-[var(--r-lg)] border border-line px-3 py-2">
<p className="truncate text-[0.82rem] text-ink-muted">{n.quote}</p>
<Button variant="link" size="sm" className="shrink-0 px-0">
Reply
</Button>
</div>
) : null}
{n.kind === "upload" ? (
<div className="mt-2 flex items-center gap-2.5 rounded-[var(--r-lg)] border border-line px-3 py-2">
<span className="flex h-8 w-7 shrink-0 items-center justify-center rounded-[var(--r-sm)] bg-[color:var(--oks-color-danger-500)] text-white">
<FileTextIcon size={14} />
</span>
<span className="min-w-0 flex-1 truncate text-[0.82rem] text-ink">{n.fileName}</span>
<span className="shrink-0 text-[0.78rem] text-ink-faint">({n.fileSize})</span>
<Button isIconOnly aria-label={`Download ${n.fileName}`} variant="ghost" size="sm">
<DownloadIcon size={15} />
</Button>
</div>
) : null}
{n.kind === "images" ? (
<div className="mt-2 grid grid-cols-2 gap-2">
{n.covers.map((cover, i) => (
<div key={i} className="relative aspect-[4/3] overflow-hidden rounded-[var(--r-lg)]" style={{ background: cover }}>
<span className="absolute bottom-1.5 right-1.5 flex h-6 w-6 items-center justify-center rounded-full bg-black/40 text-white backdrop-blur-sm">
<DownloadIcon size={12} />
</span>
</div>
))}
</div>
) : null}
{n.kind === "join-request" ? (
<div className="mt-2 flex items-center gap-2">
<Button color="primary" size="sm" radius="full">
Accept
</Button>
<Button variant="bordered" size="sm" radius="full">
Deny
</Button>
</div>
) : null}
</div>
{n.unread ? (
<span aria-label="Unread" className="mt-1.5 h-1.5 w-1.5 shrink-0 rounded-full bg-ink-faint" />
) : null}
</div>
);
}
export function NotificationsFeed() {
const [tab, setTab] = useState("all");
const today = tab === "unread" ? TODAY.filter((n) => n.unread) : TODAY;
const prevWeek = tab === "unread" ? PREV_WEEK.filter((n) => n.unread) : PREV_WEEK;
return (
<div className="mx-auto flex w-full max-w-md flex-col rounded-[var(--r-lg)] bg-paper p-5 shadow-[var(--shadow-md)]">
<div className="flex items-center justify-between">
<h2 className="text-[1.2rem] font-bold text-ink">Notifications</h2>
<Dropdown placement="bottom-end">
<DropdownTrigger>
<Button isIconOnly aria-label="Notification options" variant="ghost" radius="full" size="sm">
<MoreIcon size={18} />
</Button>
</DropdownTrigger>
<DropdownMenu aria-label="Notification options">
<DropdownItem key="read">Mark all as read</DropdownItem>
<DropdownItem key="settings">Notification settings</DropdownItem>
</DropdownMenu>
</Dropdown>
</div>
<Tabs aria-label="Filter notifications" variant="underlined" selectedKey={tab} onSelectionChange={(key) => setTab(String(key))} classNames={{ base: "mt-1" }}>
<Tab key="all" title="All" />
<Tab key="unread" title="Unread" />
<Tab key="inbox" title="Inbox" />
</Tabs>
{today.length > 0 ? (
<>
<div className="mt-3 flex items-center justify-between">
<h3 className="text-[0.85rem] font-semibold text-ink">Today</h3>
<Button variant="link" size="sm" className="px-0">
See more
</Button>
</div>
<div className="flex flex-col divide-y divide-line">
{today.map((n) => (
<NotificationRow key={n.id} n={n} />
))}
</div>
</>
) : null}
{prevWeek.length > 0 ? (
<>
<h3 className="mt-2 text-[0.85rem] font-semibold text-ink">Prev Week</h3>
<div className="flex flex-col divide-y divide-line">
{prevWeek.map((n) => (
<NotificationRow key={n.id} n={n} />
))}
</div>
</>
) : null}
</div>
);
}More patterns
App screens
Analytics dashboardA full fintech app screen — collapsible sidebar, header search, wallet cards, an earnings chart, savings goals, and a transactions table.
App screens
Settings panelA real settings screen — top tabs, two-column sections, and a sortable, selectable billing history table.
App screens
Data table viewA full documents list screen — status Tabs, a filter toolbar, and a sortable, selectable Table with real recipient avatars.