SolanaUISolanaUI

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 };