Auto-dismissing error
A transient banner that closes itself via setTimeout. Multiple calls stack — each error gets its own banner.
The Callable
declared once, mounted in the React treeimport { useEffect } from 'react'import { createCallable } from 'react-call'
interface Props { message: string durationMs: number}
const ROW_HEIGHT = 52 // px per stacked banner, including gap
export const ErrorBanner = createCallable<Props, void>( ({ call, message, durationMs }) => { useEffect(() => { const t = setTimeout(() => call.end(), durationMs) return () => clearTimeout(t) }, [call, durationMs])
// call.index reflects this call's position in the active stack — use it // to offset each banner downward so concurrent toasts don't overlap. const top = 24 + call.index * ROW_HEIGHT
return ( <div role="alert" aria-live="assertive" style={{ top }} className="pointer-events-none fixed left-1/2 z-50 -translate-x-1/2 transition-[top] duration-200" > <div className="pointer-events-auto flex w-80 max-w-[calc(100vw-1.5rem)] items-center gap-3 rounded-md border border-red-500/40 bg-red-500/10 px-4 py-2 text-sm text-red-200 shadow-2xl backdrop-blur"> <span aria-hidden="true">⚠</span> <span className="flex-1 truncate">{message}</span> <button type="button" onClick={() => call.end()} aria-label="Dismiss" className="-mr-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-base leading-none text-red-300/80 transition-colors hover:bg-red-500/20 hover:text-red-100" > × </button> </div> </div> ) },)ErrorBanner.displayName = 'ErrorBanner'The Root
mounted in your app tree, onceimport { ErrorBanner } from './ErrorBanner'import { TriggerErrorButton } from './TriggerErrorButton'
export default function App() { return ( <> <ErrorBanner /> <TriggerErrorButton /> </> )}The caller
anywhere in your app, imperativeimport { useState } from 'react'import { ErrorBanner } from './ErrorBanner'
export const TriggerErrorButton = () => { const [count, setCount] = useState(0)
const handleClick = () => { setCount((c) => c + 1) ErrorBanner.call({ message: `Network request failed (#${count + 1})`, durationMs: 1500, }) }
return ( <div className="flex flex-col items-center gap-3"> <button type="button" onClick={handleClick} className="rounded-md border border-red-500/50 bg-red-500/10 px-4 py-2 text-sm font-medium text-red-300 transition-colors hover:bg-red-500/20" > Simulate error </button> <span className="font-mono text-xs text-[var(--color-fg-subtle)]"> {count > 0 ? `${count} error${count === 1 ? '' : 's'} triggered` : 'no errors yet'} </span> </div> )}You don’t keep a list of banners
The reflex for “stacking toasts” in React is a useState<Banner[]> —
push on trigger, filter on dismiss, map to render. This example keeps no
such array. Every ErrorBanner.call(…) is an independent entry in the
Root’s Stack, and the Root renders all of them. Trigger ten errors and
that’s ten calls; the Stack is the list, and it lives in the library,
not in your component.
Because each call is its own Stack entry, it carries its own
CallContext — including call.index, this call’s position in the
Stack. That’s what spaces the banners apart: the vertical offset is just
call.index * ROW_HEIGHT, read inside the Callable with no counter of
your own. This is the one example where call.index does real work;
most Callables never touch it.
When to reach for it
Use plain .call() whenever concurrent instances should coexist rather
than replace each other — error toasts, per-item notifications, anything
where a second event shouldn’t clobber the first.
When you instead want a single surface that updates in place — a
progress bar, a “saving… → saved” pill — that’s
upsert, not stacking. The dividing question
is simply: should a new call add a banner, or update the existing one?