Save and publish
A button that shows work in progress, then a success mark only once the request has actually resolved, and a toast with a retry when it fails. Built from Button, StatusMorph and Toast.
Preview
svelte
<script lang="ts">
import { Button } from "fancy-ui-svelte";
import { StatusMorph, type StatusMorphProps } from "fancy-ui-svelte";
import { Toaster, toast, dismissToast } from "fancy-ui-svelte";
// Stand-in for your request: resolves after a short delay, or rejects while
// "fail the next save" is ticked.
let failNext = $state(false);
function saveDraft(): Promise<void> {
return new Promise((resolve, reject) => {
setTimeout(() => (failNext ? reject(new Error("offline")) : resolve()), 1200);
});
}
let status = $state<NonNullable<StatusMorphProps["state"]>>("idle");
async function publish() {
if (status === "loading") return;
status = "loading";
try {
await saveDraft();
// Success is only ever shown once the request has resolved.
status = "success";
} catch {
status = "error";
const id = toast({
title: "Could not publish",
description: "Check your connection and try again.",
variant: "error",
action: {
label: "Retry",
onClick: () => {
dismissToast(id);
void publish();
},
},
});
}
}
</script>
<Toaster />
<div class="flex flex-wrap items-center gap-4">
<Button onclick={publish}>
Publish
{#snippet iconEnd()}
<StatusMorph bind:state={status} />
{/snippet}
</Button>
<label class="text-muted-foreground flex items-center gap-2 text-sm">
<input type="checkbox" bind:checked={failNext} />
Fail the next save
</label>
</div>Steps
- Guard the handler so a second activation while the request runs is ignored, and leave the busy state to StatusMorph in the button's end slot. Setting the Button `loading` as well would draw two spinners at once.
- Drive StatusMorph from the same status: `loading` while pending, `success` after the promise resolves, `error` after it rejects. Never set `success` before the request settles.
- On failure, raise an `error` toast with a `Retry` action. The action does not close the toast by itself, so dismiss it from the handler and run the request again.
- Let StatusMorph return to idle on its own (`resetAfter`); the button is ready for the next save without any extra state.
Pitfalls
- Showing success optimistically: if the request then fails, the interface has already told the user the work is saved.
- Disabling the button while loading: it drops focus and hides the busy state from assistive technology. Keep it enabled and guard the handler instead.
- Leaving the error toast to time out: it carries the only way to retry, so it should stay until the user acts or closes it.