Templates
Notification centre
The bell in the top bar; an unread count and a panel with All and Unread, mark as read and an empty state.
The notification centre shows what happened for the person without leaving the page. The bell carries the unread count; the panel is a Popover and closes on an outside press or Escape. Use a Toast for an immediate result and this panel for the lasting history (Notifications).
Preview
Section titled “Preview”Press the bell. Pressing a row marks that notification read and the count drops; pick “Unread” to see only new ones; “Mark all as read” clears them all. The brand and mode switches in the page header restyle the preview too.
pnpm add @bigbang/ui| Data | Type | Note |
|---|---|---|
| Notifications | { id, icon, title, meta, unread }[] |
Newest first; pressing a row turns unread off. |
state |
'some' | 'empty' |
empty starts with the empty state. |
defaultOpen |
boolean |
Starts with the panel open. |
The AppShell template puts this component in the top bar, between search and the account menu.
import { Badge, Button, EmptyState, Icon, IconButton, Inline, Link, ListItem, Popover, SegmentedControl, Stack, VisuallyHidden } from '@bigbang/ui';import type { IconName } from '@bigbang/ui';import { useState } from 'react';import type { Lang } from '../../patterns/lang';
const copy = { tr: { title: 'Bildirimler', trigger: (unread: number) => (unread > 0 ? `Bildirimler, ${unread} okunmamış` : 'Bildirimler'), show: 'Göster', all: 'Tümü', unreadOnly: 'Okunmamış', unread: 'Okunmamış', markAll: 'Tümünü okundu işaretle', markedAll: 'Tüm bildirimler okundu olarak işaretlendi.', seeAll: 'Tüm bildirimler', emptyTitle: 'Yeni bildirim yok', emptyText: 'Size atanan görevler ve ekipten gelen yanıtlar burada görünür.', unreadEmptyTitle: 'Okunmamış bildirim yok', unreadEmptyText: 'Hepsini okudunuz. Eski bildirimler "Tümü" altında.', items: [ { icon: 'user', title: 'Kullanıcı B sizi Proje 1 ekibine ekledi', meta: '5 dk önce' }, { icon: 'pencil', title: 'Kayıt 12 üzerinde yeni yorum var', meta: '1 saat önce' }, { icon: 'clock', title: 'Görev 3 için son gün yarın', meta: '3 saat önce' }, { icon: 'circle-check', title: 'Dışa aktarma tamamlandı', meta: 'Dün' }, { icon: 'settings', title: 'Çalışma alanı ayarları güncellendi', meta: '2 gün önce' }, ], }, en: { title: 'Notifications', trigger: (unread: number) => (unread > 0 ? `Notifications, ${unread} unread` : 'Notifications'), show: 'Show', all: 'All', unreadOnly: 'Unread', unread: 'Unread', markAll: 'Mark all as read', markedAll: 'All notifications marked as read.', seeAll: 'All notifications', emptyTitle: 'No new notifications', emptyText: 'Tasks assigned to you and replies from your team show up here.', unreadEmptyTitle: 'No unread notifications', unreadEmptyText: 'You have read them all. Older ones are under "All".', items: [ { icon: 'user', title: 'User B added you to the Project 1 team', meta: '5 min ago' }, { icon: 'pencil', title: 'New comment on Record 12', meta: '1 hour ago' }, { icon: 'clock', title: 'Task 3 is due tomorrow', meta: '3 hours ago' }, { icon: 'circle-check', title: 'Export finished', meta: 'Yesterday' }, { icon: 'settings', title: 'Workspace settings updated', meta: '2 days ago' }, ], },};
type Item = { id: number; icon: IconName; title: string; meta: string; unread: boolean };
export type NotificationsCentreProps = { lang?: Lang; /** `empty` starts with no notifications. */ state?: 'some' | 'empty'; /** Opens the panel on first render. */ defaultOpen?: boolean;};
/** * The notification bell of the app header: an unread count on the button and a Popover with an * All / Unread switch, a list where pressing a row marks it read, "Mark all as read" and an empty state. */export function NotificationsCentre({ lang = 'tr', state = 'some', defaultOpen = false }: NotificationsCentreProps) { const t = copy[lang]; const [items, setItems] = useState<Item[]>(() => state === 'empty' ? [] : t.items.map((item, index) => ({ ...item, id: index, icon: item.icon as IconName, unread: index < 3 })), ); const [filter, setFilter] = useState('all'); const [announcement, setAnnouncement] = useState(''); const unread = items.filter((item) => item.unread).length; const shown = filter === 'unread' ? items.filter((item) => item.unread) : items;
const markRead = (id: number) => setItems((list) => list.map((item) => (item.id === id ? { ...item, unread: false } : item))); const markAll = () => { setItems((list) => list.map((item) => ({ ...item, unread: false }))); setAnnouncement(t.markedAll); };
return ( <span className="relative inline-flex"> <Popover title={t.title} side="bottom" align="end" size="lg" defaultOpen={defaultOpen} onOpenChange={(open) => { if (!open) setAnnouncement(''); }} trigger={<IconButton icon="bell" label={t.trigger(unread)} variant="tertiary" size="sm" tooltip={false} />} > <Stack gap="md"> {items.length > 0 ? ( <SegmentedControl label={t.show} size="sm" fullWidth value={filter} onValueChange={setFilter} items={[ { value: 'all', label: t.all }, { value: 'unread', label: unread > 0 ? `${t.unreadOnly} (${unread})` : t.unreadOnly }, ]} /> ) : null} {shown.length > 0 ? ( <ListItem.List divided animate aria-label={t.title} className="-mx-sm max-h-80 overflow-y-auto"> {shown.map((item) => ( <ListItem key={item.id} size="sm" render={<button type="button" />} onClick={() => markRead(item.id)} leading={<Icon name={item.icon} tone={item.unread ? 'brand' : 'subtle'} />} title={ <> {/* The space sits outside the hidden span so the accessible name keeps it. */} {item.unread ? ( <> <VisuallyHidden>{`${t.unread}:`}</VisuallyHidden>{' '} </> ) : null} <span className={item.unread ? 'font-semibold' : undefined}>{item.title}</span> </> } subtitle={item.meta} trailing={item.unread ? <span aria-hidden="true" className="block size-2 rounded-full bg-status-brand-emphasis" /> : null} /> ))} </ListItem.List> ) : ( <EmptyState size="sm" headingLevel={3} icon={items.length === 0 ? 'bell' : 'circle-check'} title={items.length === 0 ? t.emptyTitle : t.unreadEmptyTitle} description={items.length === 0 ? t.emptyText : t.unreadEmptyText} /> )} <Inline justify="between" align="center" gap="sm" wrap> {items.length > 0 ? ( <Button variant="tertiary" size="sm" startIcon={<Icon name="check" />} onClick={markAll} disabled={unread === 0} focusableWhenDisabled> {t.markAll} </Button> ) : ( <span /> )} <Link href="#" size="sm"> {t.seeAll} </Link> </Inline> <VisuallyHidden role="status">{announcement}</VisuallyHidden> </Stack> </Popover> {unread > 0 ? ( // The count is part of the button's name; the badge only shows it. <Badge aria-hidden="true" tone="brand" variant="bold" size="sm" className="pointer-events-none absolute -end-xs -top-xs min-w-5 justify-center"> {unread} </Badge> ) : null} </span> );}Structure
Section titled “Structure”- A bell IconButton with a bold Badge on it that shows the unread count.
- A Popover (
size="lg",align="end") titled “Notifications”. - A SegmentedControl “Show”: All / Unread (with the count).
- A ListItem list of button rows: icon, title and time; unread ones bold with a dot at the end.
- A small EmptyState with an
h3when the list is empty. - Footer: “Mark all as read” and an “All notifications” Link.
Motion
Section titled “Motion”The panel fades and scales in under the bell with motion.popover. Rows enter with a short stagger from motion.listItem. With reduced motion, scaling and sliding stop and only a short fade stays.
Accessibility
Section titled “Accessibility”- The bell’s name holds the count: “Notifications, 3 unread”. The badge is
aria-hidden, so the count is not read twice and not shown by colour alone. - The panel is a
dialognamed by its title; on open, focus moves to its first control, and Escape closes it and returns focus to the bell. - Unread rows start with a hidden “Unread:” prefix; bold text and the dot never carry the meaning alone.
- The result of “Mark all as read” is read from a polite status region in the panel. With nothing unread, the button is disabled but stays focusable.
- The empty state title is an
h3, under the pageh1and the panel title.
Components used
Section titled “Components used”Popover, IconButton, Badge, SegmentedControl, ListItem, EmptyState, Button, Link, VisuallyHidden. Patterns: Notifications, Empty states.