Loading page…
Loading page…
The default channel for action outcomes. Broadcast, copy, swap, and sign-out report through one app-level stack. Ordinary toasts report and leave; ApprovalToast is the explicit action-required exception, staying open as it morphs into a full human review.
ApprovalToast is not a transient confirmation. It starts as a normal summary, opens immediately into ToolApproval, and has no timeout, swipe, or close path. A recorded approve, reject, or cancel decision is required before it leaves.
Action queue
Review opens immediately. The gate has no timeout, swipe, or close button.
<ApprovalToast tool="treasury.request_disbursement" parameters={parameters} onApprove={approve} onDeny={reject} onCancel={cancel} />--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerMount ToastProvider once at the app root; it owns the queue and mounts the Toaster. Any component below calls useToast() — no prop drilling, no second stack. These buttons raise real toasts into this app's stack, bottom-right of the viewport.
Watch the bottom-right corner of the viewport, not this frame.
// app/layout.tsx — once, at the root.
import { ToastProvider } from "@coldharbor/webui";
<ToastProvider>{children}</ToastProvider>
// anywhere below it.
import { useToast } from "@coldharbor/webui";
const { showToast } = useToast();
async function copyAddress() {
try {
await navigator.clipboard.writeText(address);
showToast({ status: "success", title: "Address copied" });
} catch {
showToast({ status: "error", title: "Copy failed", description: "Clipboard is unavailable." });
}
}--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerSlow work opens a loading toast with duration 0 and dismissible false, then updateToast turns the same id into success or error. One line on screen for the whole voyage — no stack of stale progress. Every third broadcast fails, so both outcomes appear.
const { showToast, updateToast } = useToast();
const id = showToast({
status: "loading",
title: "Broadcasting transaction",
duration: 0,
dismissible: false,
});
try {
const txid = await broadcast(psbt);
updateToast(id, { status: "success", title: "Transaction broadcast", description: txid, duration: 4200, dismissible: true });
} catch (reason) {
updateToast(id, { status: "error", title: "Broadcast failed", description: String(reason), duration: 4200, dismissible: true });
}--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerA normal toast reports what an action did; it leaves as soon as it is read. Inline text reports what the user must fix and stays until fixed. Destructive confirmations belong in a ceremony. A background operation awaiting a human decision uses ApprovalToast, never a timed toast.
Inline · the field is wrong
Not a valid Bitcoin address.
Toast · the action is done
Transaction broadcast
0.245 000 00 BTC to bc1q…8p3w.
// Wrong — validation vanishes before the field is fixed.
showToast({ status: "error", title: "Not a valid Bitcoin address" });
// Right — the error stays on the field it belongs to.
<AddressField error="Not a valid Bitcoin address." />
// Right — the outcome of the action goes to the stack.
showToast({ status: "success", title: "Transaction broadcast" });--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerThe stack behind the provider, rendered statically inline (placement=static, duration 0 — no timers, no portal). Neutral, info, loading, success, error. The loading toast is non-dismissible, so it has no close affordance.
Watch-only wallet added
Main Vault (xpub…9wPd) is now tracked without keys.
Fee estimate updated
Standard is now 6 sat/vB (~30 minutes).
Broadcasting transaction
Relaying through Tor — a few seconds.
Transaction broadcast
0.245 000 00 BTC to bc1q…8p3w · 924 sats fee.
Broadcast failed
Fee below relay minimum. Nothing left your wallet.
// Prefer useToast(). Reach for the raw stack only for a local, anchored one.
import { AnimatedToastStack, useAnimatedToastStack } from "@coldharbor/webui";
const { toasts, showToast, dismissToast } = useAnimatedToastStack();
showToast({
status: "success",
title: "Transaction broadcast",
description: "0.245 000 00 BTC to bc1q…8p3w · 924 sats fee.",
});
<AnimatedToastStack toasts={toasts} onDismiss={dismissToast} position="bottom-right" fixed />--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerAn action slot for follow-ups, a persistent loading toast that cannot be dismissed, and placement=absolute anchoring the stack inside a panel instead of the viewport.
Swap complete
0.08 BTC → 28.301 XMR settled to your primary account.
Restoring wallet
Scanning 84,120 blocks for silent-payment outputs.
Address copied
showToast({
status: "success",
title: "Swap complete",
action: { label: "View swap", onClick: openSwapDetail },
});
showToast({ status: "loading", title: "Restoring wallet", dismissible: false, duration: 0 });--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerA local stack anchored inside a panel — the escape hatch when a surface owns its own toasts instead of the app queue. Entrance spring, 4.2s auto-dismiss, at most four visible, drag sideways to fling one away. Every third broadcast fails, so both outcome tones appear.
// Local stack — app-wide outcomes should use useToast() instead.
const { toasts, showToast, dismissToast } = useAnimatedToastStack({ limit: 4 });--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-dangerThe success toast under all three layers.
Transaction broadcast
0.245 000 00 BTC to bc1q…8p3w.
--ch-surface--ch-border--ch-fg--ch-fg-muted--ch-primary--ch-success--ch-danger