Txn Toast
A toast notification function for displaying transaction status updates with explorer links
import { txnToast } from "@/components/sol/txn-toast";
// Show a confirmed transaction toast
txnToast({
signature: "5UfDq3kPmE8yR4vN7bXjT2wZcA9fGhL6nKpQrS1dM8xY",
status: "confirmed",
});
// Show a pending transaction toast
const toastId = txnToast({
title: "Swapping SOL for USDC",
status: "pending",
});
// Update the pending toast to confirmed
txnToast.update(toastId, {
signature: "5UfDq3kPmE8yR4vN7bXjT2wZcA9fGhL6nKpQrS1dM8xY",
status: "confirmed",
});Installation
pnpm dlx shadcn@latest add @solanaui/txn-toast
npx shadcn@latest add @solanaui/txn-toast
yarn dlx shadcn@latest add @solanaui/txn-toast
Usage
import { txnToast } from "@/components/sol/txn-toast";
// Confirmed - auto-generates explorer link from signature
txnToast({
signature: "5UfDq3kPm...",
status: "confirmed",
});
// Pending - shows a spinner, stays visible until dismissed or updated
const toastId = txnToast({
title: "Swapping SOL for USDC",
status: "pending",
});
// Error - red icon, auto-dismisses after 5s
txnToast({
title: "Swap failed",
description: "Insufficient balance",
status: "error",
});
// Custom title + explorer URL
txnToast({
title: "Staked 100 SOL",
signature: "5UfDq3kPm...",
explorerUrl: "https://solscan.io/tx/5UfDq3kPm...",
status: "confirmed",
});Updating a Pending Toast
Use txnToast.update() to transition a pending toast to confirmed or error in-place, without dismissing and re-creating it.
// Fire a pending toast and store the ID
const toastId = txnToast({
title: "Swapping SOL for USDC",
status: "pending",
});
// Later, update the same toast with the result
txnToast.update(toastId, {
signature: "5UfDq3kPm...",
status: "confirmed",
});
// Or update to error
txnToast.update(toastId, {
title: "Swap failed",
description: "Slippage tolerance exceeded",
status: "error",
});Each toast includes a close button so users can dismiss it manually at any time.
Source Code
"use client";import { CheckCircle2Icon, ExternalLinkIcon, Loader2Icon, XCircleIcon, XIcon,} from "lucide-react";import { toast } from "sonner";interface TxnToastProps { title?: string; description?: string; signature?: string; status?: "pending" | "confirmed" | "error"; explorerUrl?: string;}const statusConfig = { pending: { icon: <Loader2Icon className="size-4 animate-spin text-muted-foreground" />, defaultTitle: "Transaction pending", defaultDescription: "Waiting for confirmation...", }, confirmed: { icon: <CheckCircle2Icon className="size-4 text-emerald-500" />, defaultTitle: "Transaction confirmed", defaultDescription: "Your transaction was successful.", }, error: { icon: <XCircleIcon className="size-4 text-red-400" />, defaultTitle: "Transaction failed", defaultDescription: "Something went wrong. Please try again.", },};const truncateSignature = (sig: string) => { if (sig.length <= 12) return sig; return `${sig.slice(0, 6)}...${sig.slice(-4)}`;};const renderToast = (props: TxnToastProps, toastId: string | number) => { const { title, description, signature, status = "confirmed", explorerUrl, } = props; const config = statusConfig[status]; const resolvedExplorerUrl = explorerUrl ?? (signature ? `https://solscan.io/tx/${signature}` : undefined); return ( <div className="flex gap-3 w-[356px] rounded-lg border bg-background p-4 shadow-lg"> <div className="mt-0.5 shrink-0">{config.icon}</div> <div className="flex flex-1 flex-col gap-1"> <span className="text-sm font-medium"> {title ?? config.defaultTitle} </span> <span className="text-sm text-muted-foreground"> {description ?? config.defaultDescription} </span> {resolvedExplorerUrl && ( <a href={resolvedExplorerUrl} target="_blank" rel="noopener noreferrer" className="inline-flex items-center gap-1 text-xs text-muted-foreground hover:text-foreground transition-colors" > {signature ? truncateSignature(signature) : "View transaction"} <ExternalLinkIcon className="size-3" /> </a> )} </div> <button type="button" onClick={() => toast.dismiss(toastId)} className="shrink-0 mt-0.5 text-muted-foreground hover:text-foreground transition-colors" > <XIcon className="size-3.5" /> </button> </div> );};const txnToast = (props: TxnToastProps) => { const status = props.status ?? "confirmed"; return toast.custom((id) => renderToast(props, id), { duration: status === "pending" ? Infinity : 5000, });};txnToast.update = (id: string | number, props: TxnToastProps) => { const status = props.status ?? "confirmed"; toast.custom((toastId) => renderToast(props, toastId), { id, duration: status === "pending" ? Infinity : 5000, });};export type { TxnToastProps };export { txnToast };