Skip to content
Tier

Result button

Pro

A button whose own face says how the work it started went: the action ("Save"), then working ("Saving…", with a spinner), then the result ("Saved", with a tick, or "Couldn't save", with a cross). The state is the app's, from the real result: set done only once the service has confirmed it, never when the request is sent, so the tick is never drawn early. The result is drawn only when the app sets it while the button is on screen.

The button is one element through every state, so focus stays on it. While working it is aria-disabled and aria-busy, with the working label as its name (never disabled, which would drop focus), and a press does nothing. A press does nothing either while the app sets aria-disabled (or wraps it in Locked), by pointer or keyboard; the button stays focusable and says why. The result is announced once, when it arrives. resetAfter returns the face to the action some time after done.

form says how the face changes: swap (the default) swaps the icon and crossfades the label; morph also turns the button the result's colour; slide slides the action's label out and the result's in.

Markdown
Version 5.2.0· other versions

This page is for 5.2.0, the current release. An earlier release’s page is the Markdown that release published, so the docs match the version you have installed (candor-lock.json records it).

Not in the docs of 1.3.0, 1.4.0, 1.5.0, 1.5.1, 1.6.0, 1.7.0, 1.8.0, 1.8.1, 1.9.0, 1.9.1, 2.0.0, 2.1.0, 2.2.0, 2.3.0, 2.4.0, 2.5.0, 2.5.1, 2.6.0, 2.6.1, 2.6.2, 2.7.0, 2.8.0, 2.9.0, 2.10.0, 3.0.0, 4.0.0, 4.0.1, 4.0.2, 4.1.0, 4.2.0 and 4.3.0: the page came later, or had another name then.

Docs pages weren’t kept for 1.0.0, 1.0.1, 1.1.0, 1.1.1, 1.2.0 and 1.2.1: they are kept from 1.3.0 on, and before that only llms.txt was published.

Preview

Loading the preview

Install

With the Candor CLI

First time? Get started sets up the registry and theme. Or with shadcn directly: npx shadcn@latest add @candor/result-button

Brings in: button, locale, motion, tokens, use-announcer, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { ResultButton, type ResultButtonState } from "@/components/candor/ui/result-button"

/*
 * The service here stands in for your own: `done` is set only when it confirms the save, never when the request goes
 * out, so the tick is drawn for something that happened. The second button's service fails, and says why.
 */
const service = (ok: boolean) => new Promise<void>((resolve, reject) => setTimeout(() => (ok ? resolve() : reject(new Error("The draft was changed by someone else."))), 900))

export default function Example() {
  const [saved, setSaved] = React.useState<ResultButtonState>("idle")
  const [sent, setSent] = React.useState<ResultButtonState>("idle")
  return (
    <div className="flex flex-wrap items-center gap-3">
      <ResultButton
        state={saved}
        labels={{ idle: "Save draft", working: "Saving…", done: "Saved", failed: "Couldn't save" }}
        resetAfter={2500}
        onReset={() => setSaved("idle")}
        onClick={async () => {
          setSaved("working")
          await service(true)
          // The confirmation, not the request, sets done.
          setSaved("done")
        }}
      />
      <ResultButton
        form="morph"
        variant="outline"
        state={sent}
        labels={{ idle: "Send for review", working: "Sending…", done: "Sent for review", failed: "Couldn't send: the draft changed" }}
        onClick={async () => {
          setSent("working")
          try {
            await service(false)
            setSent("done")
          } catch {
            setSent("failed")
          }
        }}
      />
    </div>
  )
}

<ResultButton>

PropTypeDefaultDescription
staterequiredResultButtonState—Where the work is: the app's, from the real result. `done` only once the service has confirmed it.
labelsrequiredResultButtonLabels—The words for each state; the action's and the result's are required.
form"swap" | "morph" | "slide" | undefined"swap"How the face changes: `swap` (the default), `morph` or `slide`.
resetAfternumber | null | undefinednullMilliseconds after `done` to show the action again, so the button can be pressed for the next one; `null` (the default) keeps the result until the app changes `state`. A failure stays until the app changes it.
onReset(() => void) | undefined—Called when `resetAfter` has passed and the face shows the action again; set `state` back to `idle` here.

Also takes the props of Omit<React.ComponentProps<typeof Button>, "children">.

Strings

The fixed words, in English by default. Change any of them under resultButton in <LocaleProvider strings={{ … }}>; the table is exported as resultButtonStrings.

KeyEnglish
workingWorking…
failedThat didn't work

Motion

MomentFamilyWhat it does
labelvalue, fadeWith `form="swap"` or `"morph"`, the label crossfades to the new state's words when the state changes while the button is on screen. The label a button is drawn with is shown still.
resultmark, drawThe tick (done) or the cross (failed) is drawn along its path when the app sets the result while the button is on screen. A button drawn already done or failed shows its mark at once.
slidemoveWith `form="slide"`, the old label slides up and out as the new state's label slides up into its place, when the state changes while the button is on screen. Under reduced motion the label is simply replaced.
workingliveWhile the work runs, a spinner turns beside the working label. It stops when the app says the work is over, and under reduced motion it is still, with its words.

Set with --cd-motion-result-button-<moment>-* in your brand file; the families are in the design reference.

Types

ResultButtonState
Where the work a ResultButton starts has got to, as the app knows it from the real result.
"idle" | "working" | "done" | "failed"
ResultButtonLabels
A ResultButton's words for each state.
{ /** The action, as a verb: "Save", "Send invite". */ idle: string; /** While the work runs: "Saving…". Defaults to "Working…". */ working?: string | undefined; /** After the service confirmed it: "Saved". */ done: string; /** After it failed: "Couldn't save". Defaults to "That didn't work". */ failed?: string | undefined; }