Skip to content

Notification list

helixui ships a toast() API for one-off toasts. This block is the persistent variant — a tray of notifications visible until the user dismisses or they expire.

'use client';
import { useEffect, useState } from 'react';
import { Card, IconButton, Stack, Text } from '@helixui/core';
import { Close } from '@helixui/icons';
export interface Notification {
id: string;
title: string;
description?: string;
tone?: 'info' | 'success' | 'warning' | 'danger';
expiresAt?: number; // epoch ms; auto-dismiss
}
export function NotificationList({
items,
onDismiss,
}: {
items: Notification[];
onDismiss: (id: string) => void;
}) {
// Auto-dismiss timer (no setInterval — one timer per item).
useEffect(() => {
const timers = items
.filter((n) => n.expiresAt)
.map((n) => setTimeout(() => onDismiss(n.id), Math.max(0, n.expiresAt! - Date.now())));
return () => { timers.forEach(clearTimeout); };
}, [items, onDismiss]);
return (
<div role="region" aria-label="Notifications" aria-live="polite">
<Stack gap={2}>
{items.map((n) => (
<Card key={n.id} variant="outlined" style={{ padding: 'var(--helixui-space-3) var(--helixui-space-4)' }}>
<Stack direction="row" align="center" justify="between" gap={3}>
<Stack gap={0} style={{ flex: 1 }}>
<Text weight="semibold">{n.title}</Text>
{n.description ? <Text size="sm" tone="muted">{n.description}</Text> : null}
</Stack>
<IconButton aria-label="Dismiss" variant="ghost" size="sm" onClick={() => onDismiss(n.id)}>
<Close />
</IconButton>
</Stack>
</Card>
))}
</Stack>
</div>
);
}

Usage

const [items, setItems] = useState<Notification[]>([]);
<NotificationList items={items} onDismiss={(id) => setItems((xs) => xs.filter((x) => x.id !== id))} />

The aria-live="polite" announcement makes the list screen-reader friendly. For instant feedback (form-submit success), use the global toast() helper instead; this block is for persistent notifications.