Passer au contenu principal

Notification

Implemented

Notification item — avatar/icon + title + body + timestamp + unread dot. Used in notification dropdown and feed.

Categorymolecules
Sourcepatterns/molecules/notification.html
AI HintPersistent feed row for notification dropdowns/centers — not a toast (Toast owns its aria-live) and not an inline alert (Alert). The container must own aria-live='polite' and focus return after dismiss. timestamp renders verbatim — format via Intl in the consumer or pass a timestampLabel.

Persistent feed row for notification centers. The container owns the live region — see the container contract below.

Notification

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

Usage

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
/>

Alert vs. Toast vs. Notification

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.

Pattern
Best for
Dismissal
Announcement (live region)
Alert
Inline message in page flow — form validation, save confirmations, system messages next to the content they describe.
Optional, manual (dismissible + onDismiss).
Self-announcing: owns role="alert" (error) / role="status" (others).
Toast
Ephemeral overlay feedback — confirms an action, auto-dismisses (~5s), queueable, floats above the page.
Automatic (timer) plus manual.
Self-announcing: owns role="status" + aria-live="polite".
Notification
Persistent feed row in a notification center / bell dropdown — stays until the user reads or dismisses it.
Manual, per-row (onDismiss).
Announced by its container: the row is role="article"; the surrounding list owns aria-live="polite".

Badge is the fourth neighbor: use it for the count on the bell trigger, not for the rows behind it.

Container contract: announcements & focus

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

Internationalization

  • dismissLabel is required in practice with onDismiss. The dismiss button falls back to the English 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.)
  • Timestamps render raw ISO. 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.

Known issues

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:

  • The anchor variant renders <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.
  • Do not combine 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).
  • The dismiss affordance is below tap minimum and hover-revealed — a 24×24 px target (vs --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.

Props

Prop
Type
Default
Description
id*
string
—

Stable identifier (forwards to data-notification-id; passed back via onDismiss).

title*
string
—

Headline text; wired via aria-labelledby and read first by assistive tech. Also seeds the initials avatar.

timestamp*
string
—

ISO-8601 timestamp; rendered verbatim and mirrored into <time dateTime>. Invalid values render without the <time> element.

body
string
—

Supporting copy under the title (clamped to 2 lines). Wired via aria-describedby.

icon
React.ReactNode
—

Custom icon replacing the initials avatar (decorative — aria-hidden).

unread
boolean
—

Shows the teal dot and appends an sr-only "(unread)" suffix to the title.

href
string
—

When provided the row renders as a link. Do not combine with onDismiss yet — see Known issues.

onDismiss
(id: string) => void
—

Renders the dismiss button when provided; called with the row id. Pair with dismissLabel (see Internationalization).

dismissLabel
string
—

Accessible label for the dismiss button. Required in practice whenever onDismiss is set — the English default leaks on localized pages.

className
string
—

Additional CSS classes.

Props

Preview

Notification

Prop
Type
Required
Default
Description
id
string
Required
—

Stable row id — passed back to onDismiss.

title
string
Required
—

Row heading; carries an sr-only '(unread)' suffix when unread.

timestamp
string
Required
—

ISO 8601

body
string
Optional
—

Secondary body text under the title.

icon
React.ReactNode
Optional
—

Leading icon/avatar node (decorative).

unread
boolean
Optional
false

Marks the row unread — teal dot + sr-only '(unread)' in the title.

href
string
Optional
—

Makes the row a link. Known defect: renders <a role='article'> (link semantics stripped) — avoid combining with onDismiss until fixed.

onDismiss
(id: string) => void
Optional
—

Called when the dismiss button is clicked (hover-only reveal today).

dismissLabel
string
Optional
'Dismiss notification {title}'

Accessible label of the dismiss button. English default — required in practice with onDismiss on FR pages.

className
string
Optional
—

Additional CSS classes on the row.

Quick use:<Notification />

Accessibility

ARIA rolearticle

Type

1 rule

NASA Rule 7

timestamp must be ISO 8601 (NASA Rule 7)

ARIA

1 rule

WCAG 1.3.1

Row is role='article'; unread state is announced via an sr-only '(unread)' suffix on the title. The surrounding list/dropdown owns aria-live — the row does not (double-announce risk)

Usage

Import

tsxSnippet
import { Notification } from "@epasslive/design-system"

Example 1

tsxSnippet
<Notification id="n1" title="Ticket transfer received" timestamp="2026-08-24T18:30" unread body="Louis sent you 2 tickets" />

Example 2

tsxSnippet
<Notification id="n2" title="Payout issued" timestamp="2026-08-23T09:00" onDismiss={dismiss} dismissLabel={t("dismiss")} />

Anti-patterns

— Do not hardcode data in molecules

— Avoid complex business logic in molecules

Design System v4.1.0 | ePassLive