One event fanning out to email, push, SMS, team chat and webhooks by each person's preferences, every channel at its own pace; a text that fails is retried in amber before it lands, channels count what they deliver, and the latest notifications stack up as they arrive. Pauses, and stacks on phones.
When to use
- One event fanning out to email, push, SMS, chat and webhooks by each person's preferences.
- Messaging and notification products; a live view of your own deliveries.
When not to use
- Products that send one kind of message.
- People choosing their own notification settings: a settings form, not the diagram.
Usage
Notification fan-out with your own data: every prop has the sample as its default, so pass only what you have.
import { useEffect, useRef } from "react"
import { NotificationFanout, type FanoutHandle } from "@/components/dashral/blocks/notification-fanout"
const fanout = useRef<FanoutHandle>(null)
useEffect(() => outbox.on("event", (e) => fanout.current?.send(e.name, { to: e.channels, failed: e.failed })), [])
<NotificationFanout ref={fanout} mode="live" channels={channels} />API reference
NotificationFanout
The section: a heading, the fanout in a dotted panel with a Live badge and Pause, the latest notifications stacked under it, and a row of figures. Every part comes from props, with a sample fanout by default.
| Prop | Type | Default |
|---|---|---|
eyebrow | React.ReactNode | "Notifications" |
title | React.ReactNode | sample |
description | React.ReactNode | sample |
sources | FlowNode[]Where events come from, and the channels they go out on ( | sample |
channels | FlowNode[] | sample |
events | NotifyEvent[]The sample events the demo sends, one after another. | sample |
hubLabel | stringThe hub's name; under it, the count of messages delivered, from | "Notify" |
delivered | number | 77_149 |
stats | { value: string; label: string }[]The figures under the diagram; none hides the row. | sample |
mode | PipelineProps["mode"]With your own notifications instead of sample events (see Fanout's mode="live"); sent ones join the stack. | — |
ref | React.Ref<FanoutHandle> | — |
Fanout
Events fanning out to the channels people want them on. Each event comes in as a hollow dot and is routed in the hub (its outline pulses); a solid square goes out to each channel it's for, by the share of people who want that channel, each at the channel's own pace. A message that fails first (amber on its channel) is retried and goes through. Channels light up and count what they deliver, and the page hears about every delivery. Side by side when there's room, stacked on a narrow container (the channels then show their names only). One requestAnimationFrame loop writes the SVG and the cards directly. Sample events by default; with mode="live", only the ones you send (see FanoutHandle). It stops off screen, in a hidden tab, when paused and (live) with nothing on its way; with reduced motion it's a still picture.
| Prop | Type | Default |
|---|---|---|
inputs | FlowNode[]Required. | — |
hub | { label: string; description: React.ReactNode }Required. | — |
outputs | FlowNode[]Required. | — |
speed | numberHow fast records flow: 1 is the usual pace, 2 twice as fast, 0 holds them still. | 1 |
paused | booleanBrings the flow to rest where it is (easing down, not stopping in one frame). | false |
events | NotifyEvent[]The sample events coming in, one after another ("demo"). | sample |
onDeliver | (delivery: Delivery) => voidCalled as each message is delivered (after its retry, when it needed one), in the order they land. | — |
onSent | (sent: Sent) => voidCalled once all of an event's messages are delivered. | — |
mode | "demo" | "live""demo" plays sample events on its own; "live" moves one only when you send it (see FanoutHandle). | "demo" |
ref | React.Ref<FanoutHandle> | — |
className | string | — |
type FlowNode
| Prop | Type | Default |
|---|---|---|
id | string | — |
label | string | — |
description | string | — |
detail | stringMore about it, in a tooltip on hover and keyboard focus (touch screens don't show it, so nothing essential). | — |
icon | LucideIcon | — |
mark | React.ReactNodeA brand's own mark (white, on | — |
color | string | — |
metric | { unit: string; value?: number; base?: number; per?: number }A count on the card, then | — |
chance | numberA channel's share of people who get it (their preferences), how long it takes (1 is the usual), and how often it fails first. | — |
pace | number | — |
flaky | number | — |
type NotifyEvent
What happened, and who it's for, as an event comes in.
| Prop | Type | Default |
|---|---|---|
title | string | — |
detail | string | — |
to | string | — |
type Delivery
One message delivered.
| Prop | Type | Default |
|---|---|---|
id | number | — |
channel | number | — |
event | NotifyEvent | — |
retried | boolean | — |
type Sent
One event with all its messages delivered, for the stack of notifications: which channels, and how many retried.
| Prop | Type | Default |
|---|---|---|
id | number | — |
event | NotifyEvent | — |
channels | number[] | — |
retried | number | — |
type FanoutHandle
Moves events through a live fanout (mode="live").
| Prop | Type | Default |
|---|---|---|
send | (event: NotifyEvent, options?: { from?: string; to?: string[]; failed?: string[] }) => voidSends one event from the source with id | — |