Skip to content

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/react

For 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.

Reference

PropsonConfirm, prompt, confirmLabel, destructive, timeout, variant, size…

Props

InlineConfirm props
PropType
childrenRequiredReactNode

The trigger's label, and the confirm button's unless confirmLabel is set.

–
onConfirmRequired() => unknown

The action, run when the confirm button is pressed. It is awaited as an AsyncButton's onAction: resolve for success, reject or throw for an error. A second press while it runs is ignored.

–
promptstring

The question the cross and tick answer: the pair's accessible name, announced as it opens.

default "Are you sure?"

"Are you sure?"
confirmLabelstring

The tick's accessible name. Default: children when it is a string, else the question.

–
cancelLabelstring

The cross's accessible name.

default "Cancel"

"Cancel"
pendingLabelstring

Shown beside the spinner while onConfirm runs, and announced.

default "Working…"

"Working…"
successLabelstring | ((result: unknown) => string)

Shown beside the check once onConfirm resolves, and announced.

default "Done"

"Done"
errorLabelstring | ((error: unknown) => string)

Shown beside the cross when onConfirm rejects, and announced assertively.

default "Failed"

"Failed"
resetAfternumber

How long the outcome shows before the row folds back into the trigger, in ms. Default: 3 × the deliberate token.

–
destructiveboolean

The 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 | secondary | outline | ghost | danger.

default 'outline'

'outline'
size'sm' | 'default' | 'lg'

sm | default | lg: the trigger's size, and the cross and tick at the matching icon size.

default 'default'

'default'
timeoutnumber | false

How long the open question waits for an answer before it folds back, in ms. false waits until it's answered.

default 5000

5000
disabledboolean

Blocks the trigger.

default false

false
onOpenChange(open: boolean) => void

Fires when the question opens or folds back.

–
soundboolean

With sound on (YoujProvider’s sound), opening and folding back sound, and the confirm sounds as an AsyncButton does. false keeps it silent.

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

Motion tokens
MomentTokens
Open and fold backthe box morphs between the trigger’s width and the pair’s
spring.snappy

0.2s, bounce 0

0.2s, bounce 0
Contentthe trigger and the pair crossfade with a touch of scale and blur
duration.fastease.standard

140ms · 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.deliberate

560ms · × 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 group named 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 onConfirm runs 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 open and folding back plays close; the confirm sounds as an Async Button does. sound={false} keeps it silent.