Inline Confirm
A button that asks “Are you sure?” in its own place instead of opening a dialog, then runs the action.
import { InlineConfirm } from '@youj/react'
Installation & usage
npm i @youj/reactFor an app already set up with npx @youj/cli init, which installs the peers (@youj/core, @youj/motion, motion and Base UI) and wires the provider. New app? Start at Installation.
Use an Inline Confirm for a small action that is easy to trigger by mistake, such as deleting a row, revoking a key or leaving a team, where a dialog would interrupt more than it helps. For a costlier action, use a Slide to Confirm or hold on an Async Button; for one that needs explaining, use an Alert Dialog.
'use client'
import { InlineConfirm } from '@youj/react'
export function DeleteFile({ file }: { file: File }) {
return (
<InlineConfirm destructive variant="ghost" pendingLabel="Deleting…" successLabel="Deleted" onConfirm={() => deleteFile(file.id)}>
Delete
</InlineConfirm>
)
}onConfirm is awaited: resolve for success, reject or throw for an error. A second press while it runs is ignored, and while it runs the question can't be dismissed. An open question that isn't answered folds back after timeout (5 seconds, paused while the pointer is on it); pass timeout={false} to wait for an answer.
Description
A file row's own delete: press it and the button splits into a cross and a tick, right where the person was looking.
Pressed, the trigger's box morphs into two icon buttons, a cross to cancel and a tick to confirm, and focus moves to the tick. The question (prompt) is the pair's accessible name and is announced. Confirming runs onConfirm as an Async Button does: a spinner, then a check or a cross; pendingLabel, successLabel and errorLabel are announced. Once the outcome has shown, the pair folds back into the trigger.
Publishing, with a question of its own in prompt for screen readers.
Without destructive the confirm button takes the action fill. With it, the danger fill, for an action that deletes or can't be undone.
Revoking a key that is still in use.
When onConfirm rejects (or throws), the tick shakes to a cross and errorLabel is announced assertively. After resetAfter the pair folds back, and the person can try again.
Code
inline-confirm-demo.tsx, the Delete a row story. The component itself comes from the package.
Reference
PropsonConfirm, prompt, confirmLabel, destructive, timeout, variant, size…
Props
| Prop | Type | |
|---|---|---|
childrenRequired | ReactNodeThe trigger's label, and the confirm button's unless | – |
onConfirmRequired | () => unknownThe action, run when the confirm button is pressed. It is awaited as an
AsyncButton's | – |
prompt | stringThe question the cross and tick answer: the pair's accessible name, announced as it opens. default "Are you sure?" | "Are you sure?" |
confirmLabel | stringThe tick's accessible name. Default: | – |
cancelLabel | stringThe cross's accessible name. default "Cancel" | "Cancel" |
pendingLabel | stringShown beside the spinner while default "Working…" | "Working…" |
successLabel | string | ((result: unknown) => string)Shown beside the check once default "Done" | "Done" |
errorLabel | string | ((error: unknown) => string)Shown beside the cross when default "Failed" | "Failed" |
resetAfter | numberHow long the outcome shows before the row folds back into the trigger, in ms. Default: 3 × the deliberate token. | – |
destructive | booleanThe action deletes or can't be undone: the confirm button takes the danger fill. default false | false |
variant | 'default' | 'danger' | 'ghost' | 'outline' | 'secondary'The trigger's look: default 'outline' | 'outline' |
size | 'sm' | 'default' | 'lg'
default 'default' | 'default' |
timeout | number | falseHow long the open question waits for an answer before it folds back, in ms. default 5000 | 5000 |
disabled | booleanBlocks the trigger. default false | false |
onOpenChange | (open: boolean) => voidFires when the question opens or folds back. | – |
sound | booleanWith sound on ( default true | true |
Other props pass through: extends ComponentProps<'div'>. | ||
Data attributes
The root carries data-state (closed or open) and data-size (sm, default or lg). The confirm button is an Async Button with its own data-state (idle, pending, success or error) and data-variant (danger when destructive, else default). The parts are named inline-confirm-box, inline-confirm-trigger, inline-confirm-cancel and inline-confirm-action in data-slot.
MotionWidth morph, crossfade, outcome · reduced motion
Motion
| Moment | Tokens | ||
|---|---|---|---|
| Open and fold backthe box morphs between the trigger’s width and the pair’s | spring.snappy0.2s, bounce 0 | 0.2s, bounce 0 | |
| Contentthe trigger and the pair crossfade with a touch of scale and blur | duration.fastease.standard140ms · cubic-bezier(0.2, 0, 0, 1) | 140mscubic-bezier(0.2, 0, 0, 1) | |
| Outcomethe confirm button runs the Async Button’s pending, success and error states | no token | ||
Outcome holdbefore the pair folds back, unless resetAfter is set | duration.deliberate560ms · × 3 | 560ms× 3 |
Reduced motion
The trigger and the pair swap at once, with no morph or crossfade; the confirm button's outcome shows without motion.
Reduced motion
The question opens and folds back at once. Focus moves the same way.
AccessibilityFocus, Escape, announcements
Accessibility
- The trigger is a button. Pressing it (click, Enter or Space) opens the question and moves focus to the confirm button.
- The open row is a
groupnamed by the question, which is also announced politely as it opens. - Escape and Cancel fold it back and return focus to the trigger; Escape is handled here and doesn't reach a dialog or menu around it. Focus leaving the row folds it back too, and focus stays where it went.
- While
onConfirmruns the confirm button is busy, Cancel is disabled and Escape does nothing. Pending and success are announced politely, an error assertively. - When the row folds back on its own (the timeout, or after the outcome) with focus inside it, focus lands on the trigger rather than the page.
- With sound on, opening plays
openand folding back playsclose; the confirm sounds as an Async Button does.sound={false}keeps it silent.