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.
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: 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
})
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">
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>
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
})
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">
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
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' })
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.
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.
| Option | Type | Default | Description |
|---|---|---|---|
message | string | 'Are you sure?' |
The confirmation text. Rendered as plain text unless html: true. |
title | string | '' |
Optional bold first line above the message (always plain text). When present, the message is muted below it. |
okLabel | string | null | null |
OK button text. null falls back to labels.ok. |
cancelLabel | string | null | null |
Cancel button text. null falls back to labels.cancel. |
danger | boolean | false |
true = red OK button + red icon (adds the vpc-danger class). |
placement | string | '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'. |
icon | boolean | string | true |
true = default warning triangle; false = no icon; a string is injected as custom trusted markup (e.g. an inline SVG). |
width | number | string | null | null |
Panel width — a px number or any CSS length. Default fits content, capped at min(320px, 100vw − 16px). |
offset | number | 8 |
Gap in px between the anchor and the panel. |
html | boolean | false |
The message is TEXT by default; true opts in to rendering it as markup. |
theme | string | 'auto' |
'auto' | 'light' | 'dark'. 'auto' follows the page (see Theming); a pinned value wins. |
styles | boolean | true |
false = headless: no CSS is ever injected for this popconfirm. |
labels | object | { ok: 'OK', cancel: 'Cancel' } |
Fallback button labels; merged key-by-key with the defaults, so a partial labels object is fine. |
labels.ok | string | 'OK' |
OK label used when okLabel is null. Set Popconfirm.defaults.labels once to relabel every popconfirm (e.g. localization). |
labels.cancel | string | 'Cancel' |
Cancel label used when cancelLabel is null. |
onConfirm | function | null | null |
fn(triggerEl) — called after OK. See Events & callbacks. |
onCancel | function | null | null |
fn() — called on any dismissal. |
All three return the instance, so they chain. Instances also expose
el (the bound trigger element) and opts (the
merged options).
| Method | Returns | Description |
|---|---|---|
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. |
| Static | Returns / value | Description |
|---|---|---|
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.defaults | object | 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.css | string (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.themeVars | object | Maps family tokens to CSS variables: accent → --vpc-accent, radius → --vpc-radius, font → --vpc-font — how VC.config() themes popconfirms. |
Popconfirm.varScopes | array | ['.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.
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.)
| Callback | Arguments | When 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. |
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>
| Attribute | Maps to | Value |
|---|---|---|
data-vpc | message |
The confirmation text; this attribute is also the auto-init marker. An empty value falls back to defaults.message. |
data-vpc-title | title |
Bold first line above the message. |
data-vpc-ok | okLabel |
OK button text. |
data-vpc-cancel | cancelLabel |
Cancel button text. |
data-vpc-danger | danger |
Boolean. Bare attribute or any value except "false"/"0" = true. |
data-vpc-placement | placement |
top | bottom | left | right. |
data-vpc-icon | icon |
Boolean only — "false"/"0" hides the icon. Custom icon markup is JS-only. |
data-vpc-theme | theme |
light | dark | auto. |
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.
| Property | Light default | Dark default | Used 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-shadow | soft two-layer shadow | deeper two-layer shadow | Panel drop shadow. |
--vpc-radius | 12px | 12px |
Panel corner radius. |
--vpc-font | system-ui stack | system-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
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.
| Key | Action |
|---|---|
| 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). |
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.
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 () { … }
})
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>