Ticket transfer received (unread)
Marie-Claude sent you 2 tickets for OSM Outdoor 2026.
Notification item — avatar/icon + title + body + timestamp + unread dot. Used in notification dropdown and feed.
patterns/molecules/notification.htmlPersistent feed row for notification centers. The container owns the live region — see the container contract below.
molecules
One notification row: initials avatar (or custom icon), title, optional body and timestamp, unread indicator, and optional link or dismiss affordance.
Ticket transfer received (unread)
Marie-Claude sent you 2 tickets for OSM Outdoor 2026.
On-sale reminder
OSM Outdoor 2026 tickets open Friday at 10:00.
Payment receipt
Render one Notification per item inside your notification surface; the surrounding list owns grouping, filtering, and dismissal state. title and timestamp are required with no defaults — a bare <Notification /> crashes in getInitials(undefined).
import { Notification } from '@epasslive/design-system'
<Notification
id="notif-transfer"
title="Ticket transfer received"
body="Marie-Claude sent you 2 tickets."
timestamp="2026-08-24T18:30"
unread
/>Three feedback surfaces overlap; pick by persistence and who owns the announcement. Rule of thumb: in-flow status → Alert, transient overlay → Toast, durable per-user feed item → Notification.
Badge is the fourth neighbor: use it for the count on the bell trigger, not for the rows behind it.
Each row renders role="article" with aria-labelledby (title) and aria-describedby (body). The molecule never announces itself — there is no live region on the row. The surrounding list or dropdown must own aria-live="polite" (or role="log") so newly arriving rows are announced exactly once. Do not add per-row role="status": under a live container it double-announces every row.
onDismiss(id) only reports — the parent owns removal. After removing a row, move focus to the next remaining row, or back to the trigger (the bell button) when the list empties. Removing the focused row without re-focusing drops the screen-reader cursor to the top of the page.
// The container — not the molecule — owns aria-live and focus return.
<ul aria-live="polite" className="divide-y divide-ep-border-subtle">
{items.map((n) => (
<li key={n.id}>
<Notification
{...n}
onDismiss={dismiss}
dismissLabel={t('notifications.dismiss')}
/>
</li>
))}
</ul>
// In dismiss(id): filter the row out, then move focus to the
// next row (or the bell trigger if the feed is now empty).Dismiss notification {title}. On French pages that leaks English into the accessible name — always pass a localized dismissLabel from your i18n layer. (The storefront catalogs do not ship a notifications.dismiss key yet; pass the translated string at the call site.)timestamp prints verbatim (2026-08-24T18:30) — machine format, never localized. Do not pass a pre-formatted locale string (24 août 2026): it fails ISO-8601 validation and silently drops the <time> element. Localized display needs a dedicated display-label prop (planned, not yet in the interface) or a client-side wrapper that formats via Intl.The href variant (demoed in row 2 above) ships two WCAG defects that are scheduled for the component fix pass — documented here so the demo is not mistaken for a clean accessibility reference:
<a role="article"> — the role overrides the anchor's implicit link semantics, so screen readers announce "article" instead of "link" (WCAG 4.1.2). Fix planned: drop the role override on the anchor variant.href with onDismiss yet. The dismiss <button> renders inside the anchor — nested interactive elements, invalid HTML, ambiguous hit targets. Until fixed, render link rows without a dismiss button (or plain rows with dismiss).--ep-tap-min 44 px, WCAG 2.5.5) revealed only on hover/focus, so it is invisible on touch. Also scheduled for the fix pass.id*stringStable identifier (forwards to data-notification-id; passed back via onDismiss).
title*stringHeadline text; wired via aria-labelledby and read first by assistive tech. Also seeds the initials avatar.
timestamp*stringISO-8601 timestamp; rendered verbatim and mirrored into <time dateTime>. Invalid values render without the <time> element.
bodystringSupporting copy under the title (clamped to 2 lines). Wired via aria-describedby.
iconReact.ReactNodeCustom icon replacing the initials avatar (decorative — aria-hidden).
unreadbooleanShows the teal dot and appends an sr-only "(unread)" suffix to the title.
hrefstringWhen provided the row renders as a link. Do not combine with onDismiss yet — see Known issues.
onDismiss(id: string) => voidRenders the dismiss button when provided; called with the row id. Pair with dismissLabel (see Internationalization).
dismissLabelstringAccessible label for the dismiss button. Required in practice whenever onDismiss is set — the English default leaks on localized pages.
classNamestringAdditional CSS classes.
idstring—Stable row id — passed back to onDismiss.
titlestring—Row heading; carries an sr-only '(unread)' suffix when unread.
timestampstring—ISO 8601
bodystring—Secondary body text under the title.
iconReact.ReactNode—Leading icon/avatar node (decorative).
unreadbooleanfalseMarks the row unread — teal dot + sr-only '(unread)' in the title.
hrefstring—Makes the row a link. Known defect: renders <a role='article'> (link semantics stripped) — avoid combining with onDismiss until fixed.
onDismiss(id: string) => void—Called when the dismiss button is clicked (hover-only reveal today).
dismissLabelstring'Dismiss notification {title}'Accessible label of the dismiss button. English default — required in practice with onDismiss on FR pages.
classNamestring—Additional CSS classes on the row.
<Notification />import { Notification } from "@epasslive/design-system"<Notification id="n1" title="Ticket transfer received" timestamp="2026-08-24T18:30" unread body="Louis sent you 2 tickets" /><Notification id="n2" title="Payout issued" timestamp="2026-08-23T09:00" onDismiss={dismiss} dismissLabel={t("dismiss")} />— Do not hardcode data in molecules
— Avoid complex business logic in molecules