Vanilla UI Kit Popconfirm v1.0.0 · tooltip →

Are you sure? Inline.

A confirmation anchored to the button that asked — lighter than a modal. Focus starts on Cancel, Escape backs out, and intercepted clicks only proceed once you say OK. Every card below is live.

Promise-based ask()

Anchor a one-shot confirm to any element and await the answer. Escape, outside click, or Cancel resolve false.

const ok = await Popconfirm.ask(
  '#archive', 'Archive this project?')
if (ok) archive()

Danger delete

danger: true reddens the OK button and the icon — focus still lands on Cancel, so a reflexive Enter is safe.

Popconfirm.ask(btn, {
  title: 'Delete account',
  message: 'This cannot be undone.',
  okLabel: 'Delete', danger: true
})

Declarative

Buttons with data-vpc are auto-bound at load. The click is intercepted; on confirm it proceeds — watch the inline onclick fire only after OK.

<button data-vpc="Remove this member?"
        data-vpc-danger data-vpc-ok="Remove">

Form submit button

A type=submit trigger holds the form until confirmed, then submits it for real via requestSubmit() — validation included (try submitting empty).

<form>
  <input name="email" required>
  <button type="submit"
    data-vpc="Unsubscribe this address?">
</form>

Custom labels, no icon

Rename the buttons per call (or once via Popconfirm.defaults.labels) and drop the icon with icon: false.

Popconfirm.ask(btn, {
  message: 'Log out on all devices?',
  okLabel: 'Log out', cancelLabel: 'Stay',
  icon: false, width: 240
})

Placements

Four sides with auto-flip when there's no room, plus an arrow that stays pointed at the trigger even when the panel is clamped.

<button data-vpc="I open below"
        data-vpc-placement="bottom">

Persistent binding + callbacks

new Popconfirm(el, opts) keeps intercepting until destroy(). onConfirm(el) / onCancel() report each answer.

var pc = new Popconfirm('#reset', {
  message: 'Reset all settings?',
  onConfirm: function (el) { … },
  onCancel:  function () { … }
})
pc.destroy()   // unbinds

Headless & family theming

Popconfirm.defaults.styles = false injects no CSS — the interception, focus management, and ARIA stay, the look is yours (see Popconfirm.css). With the VC core loaded, one call themes every family component.

<script src="https://cdn.jsdelivr.net/gh/vanilla-ui-kit/components/popconfirm/popconfirm.js"></script>
VC.config({ accent: '#b45309' })

API reference

Constructor & usage

Persistent binding — new Popconfirm(target, options) binds to an element (or the first match of a selector) and intercepts its clicks until destroy(). Binding an already-bound element destroys the previous instance first. Without a DOM, or when the target can't be resolved, the constructor returns a shared inert instance whose whole API is a harmless no-op. Popconfirm.create(target, options) is the same thing without new.

const pc = new Popconfirm('#delete-btn', {
  message: 'Delete this file?',
  danger: true,
  onConfirm: (el) => {},   // after OK; the original click also proceeds
  onCancel: () => {}
})
pc.show(); pc.hide(); pc.destroy()   // destroy() unbinds the interceptor

One-shot promise — Popconfirm.ask(target, opts) anchors a confirm to any element and returns a Promise<boolean>. opts may be a plain string, used as the message.

const ok = await Popconfirm.ask(el /* or selector */, 'Delete this file?')
if (ok) remove()

Declarative — elements with a data-vpc attribute are auto-bound at load; see Declarative init.

Options

Every option works for the constructor, Popconfirm.ask(), and (where listed below) the data-vpc-* attributes. Change a default for every popconfirm via Popconfirm.defaults. Defaults below are quoted from the source's defaults object.

OptionTypeDefaultDescription
messagestring'Are you sure?' The confirmation text. Rendered as plain text unless html: true.
titlestring'' Optional bold first line above the message (always plain text). When present, the message is muted below it.
okLabelstring | nullnull OK button text. null falls back to labels.ok.
cancelLabelstring | nullnull Cancel button text. null falls back to labels.cancel.
dangerbooleanfalse true = red OK button + red icon (adds the vpc-danger class).
placementstring'top' 'top' | 'bottom' | 'left' | 'right'. Flips to the opposite side when there is no room, and clamps to the viewport. Unknown values fall back to 'top'.
iconboolean | stringtrue true = default warning triangle; false = no icon; a string is injected as custom trusted markup (e.g. an inline SVG).
widthnumber | string | nullnull Panel width — a px number or any CSS length. Default fits content, capped at min(320px, 100vw − 16px).
offsetnumber8 Gap in px between the anchor and the panel.
htmlbooleanfalse The message is TEXT by default; true opts in to rendering it as markup.
themestring'auto' 'auto' | 'light' | 'dark'. 'auto' follows the page (see Theming); a pinned value wins.
stylesbooleantrue false = headless: no CSS is ever injected for this popconfirm.
labelsobject{ ok: 'OK', cancel: 'Cancel' } Fallback button labels; merged key-by-key with the defaults, so a partial labels object is fine.
labels.okstring'OK' OK label used when okLabel is null. Set Popconfirm.defaults.labels once to relabel every popconfirm (e.g. localization).
labels.cancelstring'Cancel' Cancel label used when cancelLabel is null.
onConfirmfunction | nullnull fn(triggerEl) — called after OK. See Events & callbacks.
onCancelfunction | nullnull fn() — called on any dismissal.

Instance methods

All three return the instance, so they chain. Instances also expose el (the bound trigger element) and opts (the merged options).

MethodReturnsDescription
show()this Opens the panel programmatically (the click interceptor calls this too). No-op if this instance is already the open one, or after destroy(). Opening settles any other open popconfirm as a cancel.
hide()this Closes the panel if this instance owns it. A programmatic close counts as a cancel: onCancel fires and the intercepted click does not proceed.
destroy()this Hides (as a cancel), unbinds the click interception, removes the aria-haspopup/aria-expanded attributes, and forgets the element. Safe to call twice.

Statics & helpers

StaticReturns / valueDescription
Popconfirm.ask(target, opts)Promise<boolean> One-shot confirm anchored to target (element or selector). opts may be an options object or just the message string. See resolution semantics below.
Popconfirm.create(target, options)instance Exactly new Popconfirm(target, options), for factory-style call sites.
Popconfirm.get(target)instance | null The instance bound to the element (or selector), or null.
Popconfirm.autoInit(root?)array of instances Binds every [data-vpc] element under root (default: document), skipping ones already bound; returns the instances it created. A bad element logs an error and does not abort the rest. Runs automatically at load.
Popconfirm.defaultsobject The live defaults object documented in Options — mutate it to change every future popconfirm.
Popconfirm.salt'vc1' CSS isolation token: set your own string or false before the first popconfirm. See Theming.
Popconfirm.cssstring (getter) The full embedded stylesheet, rendered with the current salt — a starting point for headless styling or for shipping the CSS yourself.
Popconfirm.version'1.0.0' Library version.
Popconfirm.displayName'Popconfirm' Family convergence metadata (used by the optional VC core).
Popconfirm.rootClass'vpc' Root class of rendered panels (convergence metadata).
Popconfirm.themeVarsobject Maps family tokens to CSS variables: accent → --vpc-accent, radius → --vpc-radius, font → --vpc-font — how VC.config() themes popconfirms.
Popconfirm.varScopesarray ['.vpc', '.vpc[data-theme=dark]'] — the selectors where theme variables are defined (deliberately unsalted), which VC core writes its overrides to.

Popconfirm.ask promise semantics. The promise never rejects. It resolves true only when the OK button is activated. It resolves false for every dismissal: the Cancel button, Escape, a pointer-down outside the panel, or another popconfirm opening (only one is open at a time — the previous one settles as a cancel). Without a DOM (SSR) or when the target can't be resolved, it resolves false immediately — the safe answer to "are you sure?" — via a thenable fallback even where Promise itself is missing. There is no built-in async/pending state: the panel closes as soon as a button is pressed.

Events & callbacks

Popconfirm's eventing is the two callback options; it dispatches no DOM CustomEvents of its own. (The only synthetic event it fires is the re-dispatched click on the trigger after a confirmed interception — your existing click/submit handlers receive it exactly once.)

CallbackArgumentsWhen it fires
onConfirm(triggerEl) — the anchor element After OK is pressed, just before the intercepted click is re-dispatched (bound instances) or the ask() promise resolves true. The return value is ignored — returning a Promise or false has no effect on the panel.
onCancel(none) On any dismissal: Cancel button, Escape, outside pointer-down, the trigger toggling its own open panel shut, hide(), or being displaced by another popconfirm opening.

Declarative init

Any element with a data-vpc attribute is bound automatically at load (on DOMContentLoaded, or immediately if the script loads later). For markup added afterwards, call Popconfirm.autoInit(root) — already-bound elements are skipped. Boolean attributes are true for any value except the literal strings "false" and "0"; a bare attribute (e.g. data-vpc-danger) counts as true.

<button data-vpc="Publish now?"
        data-vpc-title="Publish"
        data-vpc-ok="Publish" data-vpc-cancel="Not yet"
        data-vpc-danger="false"
        data-vpc-placement="bottom"
        data-vpc-icon="false">Publish</button>
AttributeMaps toValue
data-vpcmessage The confirmation text; this attribute is also the auto-init marker. An empty value falls back to defaults.message.
data-vpc-titletitle Bold first line above the message.
data-vpc-okokLabel OK button text.
data-vpc-cancelcancelLabel Cancel button text.
data-vpc-dangerdanger Boolean. Bare attribute or any value except "false"/"0" = true.
data-vpc-placementplacement top | bottom | left | right.
data-vpc-iconicon Boolean only — "false"/"0" hides the icon. Custom icon markup is JS-only.
data-vpc-themetheme light | dark | auto.

Theming

With theme: 'auto' (the default), the panel follows the page using the family's resolution order: <html data-theme> / data-bs-theme attribute → a .dark / .light class on <html> → prefers-color-scheme — re-resolved live while a panel is open (attribute changes and OS scheme flips are watched). Pin per call with theme: 'light' | 'dark'. When the VC core is loaded its shared theme engine is used instead, and VC.config({ accent: '#b45309' }) themes popconfirms together with every other family component.

All colors are CSS custom properties defined on .vpc (light) and .vpc[data-theme=dark] (dark). The injected stylesheet is inserted before your page's own CSS, so a plain .vpc { --vpc-accent: … } rule in your stylesheet wins the cascade.

PropertyLight defaultDark defaultUsed for
--vpc-accent#5b5bd6#7b7bea OK button background and focus-visible outline.
--vpc-danger#e5484d#f2555a OK button and icon in danger mode.
--vpc-warning#b45309#f5a623 Default (non-danger) icon color.
--vpc-bg#ffffff#1b1d24 Panel and arrow background.
--vpc-text#1c1d21#e9eaf0 Body text and the Cancel button.
--vpc-muted#72747e#989aa6 Message color when a title is present.
--vpc-faint#e7e7ec#31343f Panel/arrow border, Cancel border and hover fill.
--vpc-shadowsoft two-layer shadowdeeper two-layer shadow Panel drop shadow.
--vpc-radius12px12px Panel corner radius.
--vpc-fontsystem-ui stacksystem-ui stack Panel font family.

CSS isolation (salting). Panels render as class="vpc vc1" and every structural rule ships salted (.vpc.vc1 .vpc-btn { … }), so host-page design systems can't override the layout — while --vpc-* variable definitions stay deliberately unsalted so your overrides keep working. Set a custom token with Popconfirm.salt = 'acme' (sanitized to word characters and dashes), or disable salting with Popconfirm.salt = false — in both cases before the first popconfirm, since the stylesheet is injected once.

Headless. Popconfirm.defaults.styles = false (or per-call styles: false) never injects CSS. You keep the full behavior — interception, focus management, ARIA, positioning — and style the markup contract below from your own CSS. Popconfirm.css (a string, rendered with the current salt) is available as a starting point, as is the dist/popconfirm.css file mentioned in the source.

.vpc.vpc-danger[data-theme="dark"][data-placement="top"].vpc-open
  .vpc-arrow
  .vpc-body
    .vpc-icon                     ← unless icon: false
    .vpc-content
      .vpc-title                  ← only when `title` given
      .vpc-msg
  .vpc-actions
    .vpc-btn.vpc-cancel
    .vpc-btn.vpc-ok

Accessibility

The panel is a role="alertdialog" whose accessible name is the message — or the title when one is given, with the message wired up via aria-describedby (aria-labelledby points at the title or message element accordingly). The arrow is aria-hidden. Bound triggers get aria-haspopup="dialog" and a live aria-expanded, plus aria-controls pointing at the panel while it is open.

Focus opens on Cancel — the safe default, so a reflexive Enter dismisses instead of confirming. On close, focus returns to the trigger, but only when it was still inside the panel (or lost entirely) — never stolen from something else the user clicked. Reduced motion is respected (prefers-reduced-motion disables the transitions). Inside an open <dialog>, the panel renders into the dialog so it shares the top layer.

KeyAction
Escape Cancels. The key is consumed entirely (propagation and default prevented), so a host <dialog> or page listener doesn't also react.
Tab / Shift+Tab Cycles focus between the two buttons — focus is trapped in the panel. If focus is elsewhere, Tab pulls it back to Cancel.
Enter / Space Activates the focused button (native button behavior — not intercepted).

Headless mode

styles: false injects nothing — you keep the full behavior (click interception, focus management, positioning), the ARIA wiring and keyboard handling, and the stable .vpc-* class hooks; you bring your own CSS. Each demo runs in its own <iframe>: the styled examples above already injected the popconfirm stylesheet into this page, so only a clean document can show truly unstyled output.

Unstyled (styles: false)

The raw markup contract — the click is still intercepted, the panel is positioned inline, but not a single rule is injected.

new Popconfirm('#del', {
  styles: false,        // zero CSS injected
  title: 'Delete file?',
  message: 'This cannot be undone.',
  danger: true,
  onConfirm: function () { … }
})

Bring your own design (Tailwind)

The same headless popconfirm mapped to a different design — panel, arrow per placement, .vpc-open transition and danger button — loaded on demand from cdn.tailwindcss.com.

<style type="text/tailwindcss">
.vpc { @apply z-10 w-64 rounded-2xl border border-slate-200
  bg-white opacity-0 shadow-2xl transition-opacity
  duration-150; }
.vpc.vpc-open { @apply opacity-100; }
.vpc-arrow { @apply absolute h-2 w-2 rotate-45 bg-white; }
.vpc[data-placement="top"] .vpc-arrow { @apply -bottom-1
  border-b border-r border-slate-200; }
.vpc[data-placement="bottom"] .vpc-arrow { @apply -top-1
  border-l border-t border-slate-200; }
.vpc-body { @apply flex items-start gap-2.5 px-4 pt-4; }
.vpc-icon { @apply mt-0.5 text-rose-500; }
.vpc-title { @apply m-0 text-sm font-semibold
  text-slate-900; }
.vpc-msg { @apply m-0 mt-1 text-[13px] leading-5
  text-slate-500; }
.vpc-actions { @apply flex justify-end gap-2 px-4 py-3; }
.vpc-btn { @apply cursor-pointer rounded-lg border
  border-slate-200 bg-white px-3 py-1.5 text-[13px]
  font-medium text-slate-600 hover:bg-slate-50; }
.vpc-btn.vpc-ok { @apply border-transparent bg-rose-600
  text-white hover:bg-rose-500; }
</style>