Skip to content
Tier

Step up dialog

Pro

A verification step before a sensitive action: the person proves it's them again, by a passkey, a fresh SSO sign-in, or a code, whichever the app provides (each shows only when its callback is given). It says what is waiting on the check, in the app's words, and that cancelling runs nothing. Nothing is chosen for the person and no method is tried on its own: the app runs the ceremony in the callback, reports a refusal through error, and closes the dialog when the check passes. Record the pass as stepup.completed in the audit log (AuditLog knows the action).

When error arrives after a code was submitted, focus moves to the code field, which rings once.

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 and 2.6.2: 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/step-up-dialog

Brings in: bidi, button, dialog, input, locale, locked, motion, tokens, utils · npm: lucide-react@^1.67.0

Usage

TSX · The example above
"use client"

import * as React from "react"
import { StepUpDialog } from "@/components/candor/step-up-dialog"
import { Button } from "@/components/candor/ui/button"

export default function Example() {
  const [open, setOpen] = React.useState(false)
  const [busy, setBusy] = React.useState(false)
  const [error, setError] = React.useState<string>()
  const [passed, setPassed] = React.useState(false)
  const wait = () => new Promise((r) => setTimeout(r, 600))
  // The app runs each check, says why one failed, and closes the dialog when one passes (and records stepup.completed).
  const pass = () => {
    setOpen(false)
    setPassed(true)
  }
  return (
    <div className="flex flex-col items-start gap-2 text-sm">
      <Button variant="destructive" onClick={() => (setError(undefined), setOpen(true))}>
        Delete the production environment
      </Button>
      {passed && <p className="text-muted-foreground">Checked. The demo would delete it now.</p>}
      <StepUpDialog
        open={open}
        onOpenChange={setOpen}
        action="delete the production environment"
        account="sam@keel.dev"
        grantFor="10 minutes"
        codeHint="Enter the 6-digit code from your authenticator app. Try 123456."
        busy={busy}
        error={error}
        onPasskey={async () => {
          setBusy(true)
          await wait()
          setBusy(false)
          setError("This device has no passkey for sam@keel.dev. Use a code instead.")
        }}
        onVerifyCode={async (code) => {
          setBusy(true)
          await wait()
          setBusy(false)
          if (code === "123456") pass()
          else setError("That code doesn’t match. Codes change every 30 seconds.")
        }}
      />
    </div>
  )
}

<StepUpDialog>

PropTypeDefaultDescription
openrequiredboolean—Whether the dialog is open. The app opens it when a step needs a fresh check.
onOpenChangerequired(open: boolean) => void—Called with `false` when the person cancels or closes it; the step it guards does not run.
actionstring | undefined—The step waiting on the check, as a verb phrase in the app's words: "delete the production environment".
accountstring | undefined—Who is being checked, from the app: an email or a name.
onPasskey(() => void) | undefined—Shows "Use a passkey"; called when it's pressed. Run the WebAuthn ceremony here.
onSso(() => void) | undefined—Shows "Sign in again with company SSO"; called when it's pressed.
onVerifyCode((code: string) => void | Promise<void>) | undefined—Shows the code field; called with the code typed once it has `codeLength` digits and Verify is pressed.
onSendCode(() => void) | undefined—Shows "Send a new code" under the code field, for a code the app sends by email or text.
codeHintstring | undefined—Where the code comes from, in the app's words: "Enter the code from your authenticator app." Replaces the general line.
codeLengthnumber | undefined6How many digits the code has. 6 unless the app says otherwise.
grantForstring | undefined—How long the check lasts once passed, in the app's words ("10 minutes"). Left out, nothing is said about it.
busyboolean | undefinedfalseA check is in flight: the methods can't be pressed again and the pressed one shows a spinner.
errorstring | undefined—Why the last check failed, from the app. Shown under the methods.
classNamestring | undefined—Extra classes for the outer element.

Strings

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

KeyEnglish
titleConfirm it's you
descriptionBefore you {action}, check it's you.
descriptionGenericBefore this step runs, check it's you.
accountSigned in as {account}
passkeyUse a passkey
ssoSign in again with company SSO
oror
codeVerification code
codeHintEnter the {length}-digit code.
verifyVerify
lockedCheckingChecking it’s you
verifyNeedsCodeEnter all {length} digits of the code first.
newCodeSend a new code
cancelCancel
cancelNoteCancelling runs nothing: the step waits until it's checked.
grantYou won't be asked again for {grantFor}.

Motion

MomentFamilyWhat it does
busyliveWhile the app checks a code, a passkey or a sign-in, a spinner turns in the button that was pressed. It stops when the app says it is done, and under reduced motion it is still.
messagearrive, fadeA refusal fades in under the methods when the app reports one, and again when its text changes. One already showing when the dialog opens is still.

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