Promises & async
Not every action finishes instantly. toast.promise keeps people informed while an async task runs, automatically showing a spinner and swapping the copy once the work resolves or fails.
Use toast.promise
Pass any Promise and describe the text for each state. Varsel pins the toast open (duration 0) while it is loading, renders a spinner beside the copy and then restores the normal timer once the promise settles.
<script lang="ts">
import { toast } from "varsel";
const deploy = async () => {
const request = fetch("/deploy");
toast.promise(request, {
loading: {
title: "Deploying build",
description: "Packing assets and running health checks…",
},
success: (response) => ({
title: "Deployment complete",
description: "Production now serves build #" + response.id,
variant: "success",
}),
error: (error) => ({
title: "Deployment failed",
description:
error instanceof Error ? error.message : "Unknown error.",
}),
});
return request;
};
</script>
<button class="rounded-md bg-foreground h-9 px-4 py-2 text-sm font-medium text-foreground-invert hover:bg-foreground/80 transition-[background-color,scale] duration-150 ease-out active:scale-[0.975]" onclick={deploy}>
Run deploy
</button>toast.promise returns the toast id, so you can still dismiss or update it manually if the user cancels the job.
Customize each state
Each loading, success and error entry accepts either a short string or a full toast object. The success/error entries can also be functions that receive the resolved value (or the thrown error) so you can surface details from the response. Any field is fair game: variants, durations, actions, etc.
const id = toast.promise(uploadFiles(), {
loading: "Uploading assets…",
success: ({ fileCount }) => ({
title: "Upload finished",
description: `Copied ${fileCount} files to the CDN.`,
duration: 7000,
}),
error: (error) => ({
title: "Upload failed",
description: error instanceof Error ? error.message : "Unknown error.",
variant: "destructive",
}),
});Manual loading toasts
Already own the promise lifecycle elsewhere? You can still opt into the spinner by setting isLoading: true on a toast yourself. Pair it with duration: 0 so it sticks around until you update or dismiss it later.
const handleSync = async () => {
const id = toast({
title: "Syncing subscriptions",
description: "This may take a minute.",
isLoading: true,
duration: 0,
});
try {
await syncSubscriptions();
toast.success({
title: "Sync complete",
description: "All subscriptions are up to date.",
});
} catch (error) {
toast.error({
title: "Sync failed",
description:
error instanceof Error ? error.message : "Please try again.",
});
} finally {
toast.dismiss(id);
}
};Most apps can rely on toast.promise, but the manual escape hatch is there if you need full control over the lifecycle timing.