Confirm popover

Pro

A destructive button that grows into a small confirmation card, with the consequence, optional type-to-confirm, progress on the Confirm button, and a check or error before folding back.

Live · keyboard ready

Atlas settings

Jordan ReyesEditor · jordan@atlas.dev
Delete project14 deployments, 3 environments and all logs
  • A danger zone in settings: delete project, transfer ownership, revoke a member.
  • A row action in a table where a modal would lose the row's context.
  • Any destructive action that needs a sentence of consequence and a visible result without leaving the page.
  • Reversible removals such as archiving; use undo-toast so the action is instant and recoverable.
  • A one word inline confirmation in a toolbar; use confirm-morph, which morphs horizontally inside the button's row.
  • Long forms or several decisions; use dialog.

Installation

Pro source and install commands unlock with a Pro plan.

example.tsx
import { Trash2 } from "lucide-react";import { ConfirmPopover } from "@/components/arc/confirm-popover/confirm-popover"; export function DeleteProject({ project }: { project: Project }) {  return (    <ConfirmPopover      icon={<Trash2 size={16} />}      align="end"      title={`Delete ${project.name}?`}      description={`This permanently deletes ${project.deployments} deployments and every log. It can't be undone.`}      confirmText={project.slug}      confirmLabel="Delete project"      errorTitle={`Couldn't delete ${project.name}`}      onConfirm={async ({ setProgress, signal }) => {        for await (const step of api.deleteProject(project.id, { signal })) setProgress(step.done / step.total);      }}      onDone={() => router.push("/projects")}    >      Delete    </ConfirmPopover>  );}

ConfirmPopover

A destructive button that asks in place. Pressing it grows the button itself, as one surface, into a small floating card that states the consequence, with Cancel and Confirm and an optional phrase to type first. Confirm is also the progress bar: it widens as Cancel folds away and fills with the action's progress, its label inverting exactly at the fill edge. On success the card folds back into the button's shape with a check; on failure it stays open, shakes once and shows the error with Try again. Escape and a press outside cancel; focus moves into the card and back to the button.

PropTypeDefaultDescription
childrenRequiredReactNode–The trigger label, such as Delete project.
titleRequiredReactNode–The question, such as Delete Atlas?. It names the dialog.
descriptionReactNode–The consequence in one or two short sentences. It describes the dialog.
iconReactNode–A plain 16px icon before the trigger label.
confirmTextstring–A phrase to type before Confirm unlocks, such as the project name. Matching ignores case and outer spaces. Confirm fills in the tone color as the typed prefix matches, and a wrong submit shakes the card.
confirmLabelstring"Delete"Label of the confirm button.
cancelLabelstring"Cancel"Label of the cancel button.
pendingLabelstring"Deleting"Label on the progress bar while onConfirm runs, followed by the percentage when progress is reported.
successLabelstring"Deleted"Shown with a check in the button's shape once onConfirm resolves. Keep it no longer than the trigger label so the width holds.
errorTitleReactNode"That didn't work"Heading of the failure. The rejection's message is shown under it.
retryLabelstring"Try again"Label of the confirm button after a failure.
tone"danger" | "neutral""danger"danger tints the trigger and fills Confirm in the danger color. neutral uses the foreground, for serious but reversible actions.
onConfirm(ctx: ConfirmPopoverContext) => void | Promise<unknown>–Runs the action. ctx.setProgress(0 to 1) drives the bar; without it the bar sweeps. ctx.signal aborts if the component unmounts. A rejection shows the failure.
onCancel() => void–Called when the question is dismissed by Cancel, Escape or a press outside.
onDone() => void–Called once the success state has been shown and the button is back at rest.
onError(error: unknown) => void–Called with the rejection when onConfirm fails.
openboolean–Controlled open state of the question. Pair with onOpenChange.
defaultOpenbooleanfalseOpens on mount when uncontrolled.
onOpenChange(open: boolean) => void–Called when the question opens, is dismissed, or closes after success.
onPhaseChange(phase: ConfirmPopoverPhase) => void–Called with idle, confirming, working, success or error.
align"start" | "center" | "end""start"Which edge of the button the card keeps. end grows toward the start, for buttons at the end of a row. The card shifts to stay 16px inside the window either way.
side"bottom" | "top" | "auto""auto"Grow downward, upward, or upward only when there is not room below.
widthnumber320Width of the open card in px, never wider than the window less 32px.
resultDurationnumber1600Milliseconds the success state holds before the button returns to rest.
disabledboolean–Disables the trigger.
classNamestring–Class on the root element.
styleCSSProperties–Inline styles on the root element.
EnterorSpace
On the button, opens the question. Focus moves to the phrase field when there is one, otherwise to Cancel.
TaborShift+Tab
Cycles through the field, Cancel and Confirm while the card is open.
Enter
In the phrase field, confirms once the phrase matches; otherwise the card shakes and focus stays in the field.
Escape
Cancels the question or dismisses a failure, and returns focus to the button once the card has folded back.
  • The trigger has aria-haspopup="dialog" and aria-expanded. The open card is a non modal alertdialog named by the title and described by the consequence.
  • The phrase field has a visible label that includes the phrase; it is marked aria-invalid while the typed text does not match.
  • While the action runs the card is aria-busy and holds focus; a polite live region reads the pending label and the percentage, then the success label or the failure. The failure is also a role="alert" paragraph and focus moves to Try again.
  • Confirm stays in the tab order while locked and says so with aria-disabled, so people can reach it and hear why it does nothing yet.
  • The trigger is kept in place, invisible, while the card covers it, and takes focus back only after the card has folded into it, so focus is never lost to the page.
  • One surface: at rest it is drawn exactly over the button, then its width, height and corner radius spring to the card on the morph spring (a hint of bounce) and back on the smooth spring. The edge nearest the button stays still, and the content is pinned to that edge, so the card is revealed in place rather than sliding.
  • The button face fades out in 100ms as the card content fades up 6px from a 4px blur, 60ms later. Tint, border and floating shadow crossfade from the button's look to the card's.
  • Typing the phrase fills Confirm on the snappy spring to the share of the phrase matched so far; a second copy of the label in the fill's ink is clipped to the fill, so the text inverts exactly at its edge.
  • Confirming folds Cancel away on the smooth spring while Confirm widens to the full row; the fill drops to zero and then follows reported progress, or a soft sweep runs when there is none.
  • Success fills the bar, then the card folds back to the button's size with the check rotating in. Failure refills the bar as Try again, opens the error on a height spring and shakes the card once with a damped keyframe.
  • With reduced motion the size changes are instant and only the faces crossfade; there is no shake and the sweep becomes a static tint.
  • The card is 320px wide by default and never wider than the window less 32px; it shifts horizontally to stay 16px from either edge, so it works from a 320px phone upward.
  • It grows downward unless there is not enough room below the button, then upward, keeping the button's edge still.
  • The phrase field disables autocorrect, autocapitalise and spellcheck, so phones do not change what is typed.
  • Only one surface is mounted while the question is open, and nothing is mounted at rest besides the button.
  • The fill and its inverted label use a clip-path driven by a motion value, so typing and progress updates do not re-render the component.
  • Width and height animate on a single absolutely positioned layer, which does not reflow the page around it.

Notes for AI

Give your coding assistant the Markdown reference instead of screenshots.

  • Use it for irreversible or serious actions where the consequence should be read before confirming: deleting a project, revoking access, rotating keys, cancelling a subscription.
  • Write the description as the consequence, with numbers when you have them: This deletes 14 deployments and every log. It can't be undone.
  • Add confirmText only for the most destructive actions; typing the resource name is friction by design.
  • Report progress from onConfirm with ctx.setProgress when the action has steps; otherwise the bar sweeps until the promise settles. Throw an Error with a human message to show it under the error title.
  • Keep successLabel short, such as Deleted or Removed, so the button does not change width at rest.
  • The card floats over the page with position absolute inside the component. Do not place it in an ancestor with overflow hidden that is smaller than the card.

The full library index for assistants is at /llms.txt.