Back to scenario guides
Scenario guide 01

Save, submit, and publish feedback

Saving is often brief while publishing can take seconds. Both need an immediate acknowledgement, then a clear state change that explains the outcome.

Save, submit, and publish can look like one button, yet each carries a judgment about result, risk, and waiting. Complete feedback makes four things clear: input was received, the system is working, the result lands somewhere visible, and there is a path forward when work fails.

Make this decision first

Start with reversibility, whether waiting exceeds one second, and what visibly changes after success.

Implementation path

  1. 01

    Acknowledge the click

    Give a brief press response and prevent accidental duplicate submission.

  2. 02

    Make waiting legible

    For work that outlasts an instant, show an in-progress state with plain language.

  3. 03

    Resolve in place

    Place success, failure, or undo near the place where the action happened.

Classify the three kinds of waiting

A save can finish in 150 ms, where acknowledgement is enough. A submit can include validation and a network round trip. Publish, upload, and payment often last several seconds. Each duration needs its own interface language. Instant work benefits from a brief press response and state swap; short waits need a button that clearly enters progress; long waits need progress, permission to leave, or a clear place where the result will appear. One generic spinner flattens important differences.

Motion duration should follow system facts. A button press can settle in 80–140 ms so input and response feel connected; when the request begins, switch the label to Saving or Publishing. Once work finishes, success usually needs only 600–1200 ms for confirmation before the interface returns to useful work. Waiting is a matter of rhythm: people need one continuous causal chain rather than a series of unrelated effects.

Ready, publishing, published, and needs-attention states, with publishing branching to success and failure.ClickRequest resolvesRequest failsEdit or retryReadyActionavailablewith riskcontextPublishingPreventduplicatesand showprogressPublishedExpose linkand nextactionNeedsattentionKeep draftand retrypath
Diagram 1: a publish state flowReady, publishing, published, and needs-attention states, with publishing branching to success and failure.

Resolve the state where the action happened

After someone clicks Save, attention remains near the editor footer, the end of a form, or the toolbar. Resolve success there first: change the button to Saved, add a short adjacent status, and return the field boundary to a settled color. A global toast can add context, such as All changes synced, yet should not be the only confirmation. The farther feedback sits from the action, the more people must search for what just happened.

In-place feedback also makes failure easier to understand. A network error can restore the button while keeping Retry nearby; field validation can return focus to the exact input instead of sending someone to the top of the page; a failed publish should retain the draft and expose the reason. Continuity matters more than spectacle: one object moving from available to working to resolved lets people see the system handling their request. A crossfade, slight opacity change, and stable layout are enough to carry that meaning.

Include failure and duplicate actions in the flow

A feedback flow never has only a success path. People may click repeatedly, hit a timeout, lose permission during submission, or recover from offline work. Each case needs a predictable landing place in the UI. Disable duplicate submission on the primary action while keeping a path to cancel or return; do not silently restore a button after a timeout—explain whether work may still be running; surface a permission change so people do not assume content was published. Listing these edge states in the design makes a consistent state machine much easier to build.

Reversible actions deserve their own treatment. Delete, archive, and cancel invite can leave a list first, then expose Undo near the original location. The exit can have a short motion, while a stable placeholder or count change prevents the list from collapsing so abruptly that people doubt what they clicked. Final removal happens after the undo window. This gives people a way back while keeping the list’s spatial relationship clear.

Publish button, local result card, and global notice arranged from nearest to farthest.Same line of sightAdd contextButton stateFirstconfirmationlayerResult cardLink, undo,or failurereasonGlobal noticeSync orcross-pagecontext
Diagram 2: feedback proximityPublish button, local result card, and global notice arranged from nearest to farthest.

Let the page receive the user after publish

After publishing, interfaces often stop at Success and leave people on a page with no obvious next move. A complete design shows what follows: open the published page, copy the link, continue editing, check review status, or return to the content list. Motion can shift attention from the Publish button to that next action: reveal a result card in the same region, then make Copy link available. Keep the sequence short and the destination close so people can continue without reorienting.

This is why a real product moment is more useful than a lone atomic animation. A publish success moment often includes press feedback, button state, progress, a result card, and a next action; they share one rhythm and hierarchy. Start with a Pack to see the full state flow, then return to press feedback, crossfade, and duration to tune local details. That is more reliable than assembling a few attractive animations from scratch. Motion serves trust here: people can understand it and continue.

Write feedback as a testable state contract

Every flow benefits from a state table: what idle, working, complete, failed, and undoable show; which controls are available; where focus sits; and whether leaving the page is allowed. Motion attaches to state transitions rather than replacing state itself. This keeps product, design, and engineering discussing the same thing and lets tests check whether success, failure, retry, and undo truly work. Data state is the fact; animation simply makes the fact easier to see.

The contract also needs reduced motion. The default path can give a button slight scale and fade in a result card; the low-motion path keeps the same Saving and Saved copy, disabled button, and next action while shortening or removing decorative travel. Whether people use keyboard, touch, or a system reduced-motion preference, the core feedback chain should remain complete. Treat this as a pre-release check and motion becomes part of product reliability.

A timeline from press and acknowledgement through request duration to the next action after completion.0 msPressed120 msInputacknowledgedDuringrequestPublishing orprogress800 ms afterresultResult andnext action
Diagram 3: rhythm from press to resultA timeline from press and acknowledgement through request duration to the next action after completion.

Save, submit, and publish checklist

  • Acknowledge input immediately after clickPress, copy, or state color changes within roughly 140 ms.
  • Working state prevents duplicate submissionKeep any necessary cancel, return, or explanatory path.
  • Results and failures remain close to the actionButton, local state, and global notice each carry a clear responsibility.
  • Timeout, failure, and undo each have a clear landing placeDraft, error reason, and retry path remain findable.
  • Completion leads to a next stepAfter publish, offer open, copy, continue editing, or return-to-list paths.

Case: article publishing hands off within one region

The publish button first enters progress, then resolves into a result card with an Open article action.

<section class="publish-action" data-state="ready">
  <button class="publish-button" type="button" data-publish aria-describedby="publish-status">Publish article</button>
  <p id="publish-status" role="status" aria-live="polite" data-publish-status>Ready to publish.</p>
  <p>Requests sent: <output data-publish-submissions>0</output></p>
  <div class="publish-result" data-publish-result aria-hidden="true" inert>
    <strong>Article published.</strong>
    <a href="#published-article">Open published article</a>
  </div>
</section>

<style>
.publish-action { display: grid; gap: 12px; max-width: 28rem; }
.publish-button, .publish-result { transition: opacity 180ms ease-out, transform 180ms ease-out; }
.publish-result { opacity: 0; visibility: hidden; transform: translateY(6px); pointer-events: none; }
.publish-action[data-state="working"] .publish-button { opacity: .72; cursor: progress; }
.publish-action[data-state="complete"] .publish-button { opacity: 0; visibility: hidden; transform: scale(.98); pointer-events: none; }
.publish-action[data-state="complete"] .publish-result { opacity: 1; visibility: visible; transform: translateY(0); pointer-events: auto; }
@media (prefers-reduced-motion: reduce) {
  .publish-button, .publish-result { transition-duration: 1ms; }
}
</style>

<script>
const action = document.querySelector(".publish-action");
const publishButton = action.querySelector("[data-publish]");
const status = action.querySelector("[data-publish-status]");
const publishResult = action.querySelector("[data-publish-result]");
const publishedLink = publishResult.querySelector("a");
const requestCount = action.querySelector("[data-publish-submissions]");
let submissions = 0;

function publishRequest() {
  return new Promise((resolve) => window.setTimeout(resolve, 160));
}

async function publishArticle() {
  if (action.dataset.state !== "ready") return;

  action.dataset.state = "working";
  publishButton.disabled = true;
  status.textContent = "Publishing article.";
  submissions += 1;
  requestCount.textContent = String(submissions);

  try {
    await publishRequest();
    action.dataset.state = "complete";
    publishResult.inert = false;
    publishResult.setAttribute("aria-hidden", "false");
    status.textContent = "Article published. You can open it now.";
    publishedLink.focus();
  } catch {
    action.dataset.state = "ready";
    publishButton.disabled = false;
    status.textContent = "Publishing failed. Try again.";
  }
}

publishButton.addEventListener("click", publishArticle);
</script>

State lives on the parent data-state, so button and result card share one fact instead of using a timer to guess when the request ends. Visibility keeps both the pending result and the completed action out of the focus order, exposing only the next usable result; opacity and short travel handle the handoff, and reduced motion resolves it immediately.