skip to content

In a design system, what should a button's loading state do while an e-signature is being submitted, and what goes wrong without one?

level: middleimportance: should knowfreq 40%

answer

  1. the second tap
  2. same width, same place
  3. focus stays on the button
  4. busy state is perceivable
  5. the server must dedupe too

basics

~10 s

A loading state blocks repeat activation, keeps the button's size and position, keeps focus on it, and makes the busy state perceivable. Without it, signers press again, causing duplicate submissions, or assume nothing happened.

solid answer

~50 s

When a signer presses "Finish signing", the request may take seconds. The button's loading state should **ignore further presses** while the request is pending, **keep its width** so the layout does not shift under the pointer or finger, **keep focus** on the button rather than dropping it, and make the **busy state perceivable** both visually (an indicator plus a label like "Signing…") and to assistive technology. When the request finishes it restores the button and the outcome is announced; on failure the button returns with its original label and the error is explained. Without a loading state, people press again and create duplicate submissions, or leave believing nothing happened. The UI guard is not the whole defence: retries and flaky networks can still resend, so the server must treat a repeated submission as the same one.

code

pseudocode · 14 lines
pseudocode
on activate(button, agreement):
    if button.pending:
        return                                   # ignore repeat presses
    button.pending = true
    button.show_busy(label = "Signing…")        # width unchanged, focus stays
    result = submit(agreement, attempt_id = agreement.current_attempt_id)
    button.pending = false
    if result.ok:
        announce_status("Agreement signed")
        go_to(result.next_screen)
    else:
        button.restore(label = "Finish signing")
        announce_status("Signing failed: " + result.reason)
        # retry reuses the same attempt_id, so the server dedupes

go deeper

for a junior

Recall why a button needs a loading state at all: to show the press registered and to stop people pressing twice.

for a middle

Explain the full contract: repeat guard, stable width, focus retention, perceivable busy state and a failure path, and why fully disabling is a weaker substitute.

for a senior

Describe how you traced duplicate submissions to a missing guard or a missing server-side dedupe, and how you fixed both layers together.

for a principal

Consider making the pending contract a system-level behaviour every action button inherits, paired with an idempotency expectation in the platform's API guidelines.

## Why buttons need a loading state An action button starts work that can take time: a network request, a cryptographic step, a document render. In an e-signature product, pressing **Finish signing** may take several seconds. During that time the user needs to know two things: that the press registered, and that pressing again is unnecessary. A **loading state** is how the button says both. ## The behaviour contract - **Ignore repeat activation.** While a request is pending, further presses do nothing. This is the main defence against duplicate submissions from impatient double-taps. - **Keep the size and position.** The indicator replaces or accompanies the label without resizing the button, so nothing around it shifts and the pointer or finger stays on target. - **Keep focus.** Focus stays on the button. An implementation that fully disables the control can drop focus to the top of the page on some platforms, stranding keyboard and screen-reader users. - **Make the busy state perceivable.** Show an indicator and keep a text label, such as "Signing…", so the state does not rely on motion or colour alone, and expose the busy state to assistive technology. - **Restore and report.** On success, announce the outcome and move on; on failure, restore the original label, keep the user's input, and explain what went wrong. How long to wait before showing the indicator, and which indicator to use, are decisions of the loading-indicator specification; the button's job is the contract above. ## What goes wrong without it | Missing behaviour | Symptom in the e-signature flow | |---|---| | No repeat guard | Two signing events, two completion emails, a confused sender | | No visual change | Signer presses again or leaves, believing the press failed | | Width changes | Surrounding controls jump; a second tap lands on the wrong control | | Focus dropped | Keyboard user is sent to the top of the page mid-task | | No failure path | Button spins forever or resets silently; the signature may or may not exist | ## The server still has to dedupe A front-end guard handles the double-tap, but it cannot stop every duplicate: a mobile client may retry on a flaky connection, or a user may refresh and submit again. The robust design pairs the button contract with an **idempotent submission**: the request carries an identifier for this attempt, and the server treats a repeat of the same identifier as the same submission. The button prevents most duplicates; the server guarantees correctness. ## Sequence of a well-behaved submit 1. Signer presses Finish signing; the button enters its pending state and ignores further presses. 2. The label changes to "Signing…" with an indicator, at the same width; focus stays. 3. The request carries a stable attempt identifier. 4. On success, the outcome is announced and the next screen appears. 5. On failure, the button returns to "Finish signing", the reason is shown, and the signer can retry safely with the same identifier. ## Tricky cases - **The request finishes almost instantly.** Flashing a busy label for a split second is noise; the delay before showing an indicator is set by the loading-indicator specification. - **Several action buttons in one form.** Only the pressed button shows busy, but the others should also ignore activation, or "Decline" can race "Finish signing". - **The user leaves mid-request.** The outcome still has to reach them: on return, show whether the signature was recorded. - **The device goes offline.** Say so plainly and keep the input, rather than spinning indefinitely. ## What the system should specify - The pending-state visuals for every emphasis level, including destructive buttons. - The busy label convention, such as present participle plus ellipsis ("Signing…"). - That pending buttons ignore activation but remain focusable and legible. - The expectation, stated in engineering guidance, that the action behind a submit button is idempotent. ## Across platforms Native mobile apps need the same contract; if anything the risk is higher, because touch users double-tap more and connections drop more often. Whether the indicator is a spinner inside the button or a separate progress element is a platform and system choice; the guard, the stable size, focus retention and an idempotent request are not.

  • Why not just disable the button while the request runs?
    Fully disabling can drop focus from the button on some platforms, sending keyboard and screen-reader users back to the start of the page, and it usually makes the label faint. A pending state that ignores presses but keeps the control focusable and legible gives the same protection without those side effects.
  • The request fails after eight seconds. What should the button do?
    Leave the pending state, restore its original label and emphasis, keep focus, and let the error be shown in text near the action or announced. The signer's input must survive, and a retry should reuse the same attempt identifier so that if the first request actually reached the server, the retry is recognised rather than duplicated.

saying these in an interview costs you the question

  • A spinner in the button is all the loading state needs.
  • Blocking double taps in the interface fully prevents duplicate submissions.
  • It is fine for the button to shrink to the size of the spinner.
  • Disabling the button is the same as a proper pending state.
  • On failure, silently resetting the button is enough.