registry:ui · General
popconfirm — the light sure-bubble
The light confirm bubble (NOT an alertdialog): trigger = children; panel content/actions open via content/actions snippets with the current rendering as defaults. Light dismiss = cancel.
$npx jixoai-ui add popconfirm
https://ui.jixoai.com/r/popconfirm.json
overview
Overview
The quick “sure?” that deletes a row without ceremony — a popover=auto panel on the popover laws, never an alertdialog.
popconfirm is antd's highest-frequency unique gift, per the antd batch-1 ruling:
the LIGHT confirm bubble for risky-but-reversible actions. The modal weight stays
with system-dialog; this is a popover="auto" panel carrying role="dialog" — light dismiss, Escape, one-at-a-time and the top layer
are the platform's, CSS anchor positioning places it
(position-area from the placement prop with
flip-block/flip-inline try-fallbacks and anchors-visible hiding), and
the shared WAAPI surface-motion kernel animates entry/exit. The platform element
paints nothing — the theme's jx-surface owns fill, border and blur; the shadow is a
real DOM child the kernel animates in lockstep.
The DOM is TWO SIBLINGS, and that is the family's deepest law: the trigger
wrapper (an anchor-name span — compose any focusable control; the first button
auto-wires as the declarative popovertarget trigger with
aria-expanded/aria-controls mirrored) and the promoted panel. No common carrier
root exists, so the PORTAL LAW applies: the §11 carriers and the radius/elevation
consumption stamps land on the PANEL itself — the promoted root is self-carried
(navigation-menu stamps its bar and lets .jx-pop read through inheritance; this
family owns its sheet and reads its own stamps). The anchor span is a live
density scope for the trigger tenant; the panel is one for its own content —
the family is a density PROVIDER (inherit-then-provide).
Semantics are the product: light dismiss IS the cancel path — any dismissal that
is not the confirm button runs oncancel; confirm runs onconfirm then closes (try/finally — a throwing handler still
closes); focus lands on CANCEL on open, the safe action. Composition-first (the
MILDER ruling): the panel's two areas open to content / actions snippets with the current rendering as defaults — an override
owns the semantics (aria-labelledby drops; wire your own ids). Per-axis below; the
shared grammar lives on the universal props page.
popconfirm
The default panel: open it — focus lands on Cancel. Confirm runs the action; clicking outside or pressing Escape runs the cancel path instead. Either way the outcome surfaces below.
Delete this row?
The check history goes with it.
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<!-- default panel: title/description strings + confirm/cancel. An
explicit id + PressButton's own popovertarget prop wire the
declarative trigger (the auto-wire is the plain-button path) -->
<Popconfirm
id="pc-demo"
title="Delete this row?"
description="The history goes with it."
confirmLabel="Delete"
onconfirm={del}
oncancel={() => console.log('kept')}
>
<PressButton popovertarget="pc-demo">delete row</PressButton>
</Popconfirm>
<!-- opened panel: content/actions snippets replace the areas. An
explicit id lets the authored actions close through the platform —
popovertarget wires the keep button to the panel itself -->
<Popconfirm id="merge-pc" title="Merge this branch?" onconfirm={merge}>
{#snippet content()}
<p class="font-nav text-xs uppercase">merge this branch?</p>
<p class="text-[12.5px] text-muted-foreground">3 commits, all checks green.</p>
{/snippet}
{#snippet actions()}
<div class="flex justify-end gap-2">
<button type="button" popovertarget="merge-pc">keep</button>
<button type="button" onclick={merge}>merge</button>
</div>
{/snippet}
<PressButton popovertarget="merge-pc">merge branch</PressButton>
</Popconfirm>composition
Opening the panel: content / actions
The milder ruling (a compact confirm popover, not a page dialog — local-open principle): the trigger stays children, and the panel's two areas open to snippets. content replaces the title/description block (you own the semantics — wire your own aria ids); actions replaces the confirm/cancel row (close through the platform: a popovertarget button, light dismiss, or your own handler). The current rendering stays the default for both.
merge this branch?
3 commits, all checks green — fast-forward is impossible.
law
The confirmation state machine
Confirm is the only path that runs onconfirm; everything else — light dismiss, Escape, the cancel button — is the cancel path. The panel closes through the platform in every case.
| Property | Type | Default | Description |
|---|---|---|---|
| open | trigger click (declarative popovertarget) | the anchored bubble — placement with flip fallbacks | announces: role="dialog" named by the title |
| confirm | the confirm button | onconfirm runs, then the panel closes (try/finally — a throwing handler still closes) | announces: the outcome is the caller's |
| cancel | light dismiss · Escape · the cancel button | oncancel runs — ANY dismissal that is not confirm is a cancel | announces: nothing was destroyed |
| focus | open | focus lands on CANCEL (the safe action — the system-dialog law in its light form) | announces: the destructive path is never focus-trapped |
| override | content / actions snippets | the caller owns the area AND the semantics — aria-labelledby drops (wire your own ids) | announces: whatever the caller authors |
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<!-- default panel: title/description strings + confirm/cancel. An
explicit id + PressButton's own popovertarget prop wire the
declarative trigger (the auto-wire is the plain-button path) -->
<Popconfirm
id="pc-demo"
title="Delete this row?"
description="The history goes with it."
confirmLabel="Delete"
onconfirm={del}
oncancel={() => console.log('kept')}
>
<PressButton popovertarget="pc-demo">delete row</PressButton>
</Popconfirm>
<!-- opened panel: content/actions snippets replace the areas. An
explicit id lets the authored actions close through the platform —
popovertarget wires the keep button to the panel itself -->
<Popconfirm id="merge-pc" title="Merge this branch?" onconfirm={merge}>
{#snippet content()}
<p class="font-nav text-xs uppercase">merge this branch?</p>
<p class="text-[12.5px] text-muted-foreground">3 commits, all checks green.</p>
{/snippet}
{#snippet actions()}
<div class="flex justify-end gap-2">
<button type="button" popovertarget="merge-pc">keep</button>
<button type="button" onclick={merge}>merge</button>
</div>
{/snippet}
<PressButton popovertarget="merge-pc">merge branch</PressButton>
</Popconfirm>types
Confirmation variants
Use destructive confirmation by default, or switch the confirm tone for positive actions.
popconfirm · variants
Delete this row?
Merge this branch?
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<div class="grid gap-4 sm:grid-cols-2">
<div class="border border-border p-4"><Popconfirm id="pc-tone-destructive" title="Delete this row?"><PressButton popovertarget="pc-tone-destructive">destructive</PressButton></Popconfirm></div>
<div class="border border-border p-4"><Popconfirm id="pc-tone-primary" title="Merge this branch?" confirmTone="primary"><PressButton popovertarget="pc-tone-primary">primary</PressButton></Popconfirm></div>
</div>usage
Usage
The trigger stays in children; content and actions snippets are optional overrides.
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<!-- default panel: title/description strings + confirm/cancel. An
explicit id + PressButton's own popovertarget prop wire the
declarative trigger (the auto-wire is the plain-button path) -->
<Popconfirm
id="pc-demo"
title="Delete this row?"
description="The history goes with it."
confirmLabel="Delete"
onconfirm={del}
oncancel={() => console.log('kept')}
>
<PressButton popovertarget="pc-demo">delete row</PressButton>
</Popconfirm>
<!-- opened panel: content/actions snippets replace the areas. An
explicit id lets the authored actions close through the platform —
popovertarget wires the keep button to the panel itself -->
<Popconfirm id="merge-pc" title="Merge this branch?" onconfirm={merge}>
{#snippet content()}
<p class="font-nav text-xs uppercase">merge this branch?</p>
<p class="text-[12.5px] text-muted-foreground">3 commits, all checks green.</p>
{/snippet}
{#snippet actions()}
<div class="flex justify-end gap-2">
<button type="button" popovertarget="merge-pc">keep</button>
<button type="button" onclick={merge}>merge</button>
</div>
{/snippet}
<PressButton popovertarget="merge-pc">merge branch</PressButton>
</Popconfirm>api
API
Popconfirm props separate the trigger, default copy, callbacks, placement, and snippet escape hatches. The anchor span carries rest passthrough (the trigger lane's host); the panel's popover/id wiring is load-bearing family law.
Properties
| Property | Type | Default | Description |
|---|---|---|---|
| title * | string | — | The question — one line, past-tense verb. DEFAULT rendering only (a content snippet replaces it). |
| description | string | — | Optional supporting line — DEFAULT rendering only. |
| onconfirm | () => void | — | Runs on confirm before close — try/finally: a throwing handler still closes. |
| oncancel | () => void | — | Runs on ANY non-confirm dismissal — light dismiss and Escape included. |
| confirmLabel | string | 'Confirm' | DEFAULT rendering only. |
| cancelLabel | string | 'Cancel' | DEFAULT rendering only — also the open-focus target. |
| confirmTone | 'destructive' | 'primary' | 'destructive' | The loud path is the default; primary is the opt-in for positive confirmations. |
| placement | 'top' | 'bottom' | 'left' | 'right' | 'top' | position-area from the anchor — flip-block/flip-inline fallbacks keep the panel visible; anchors-visible hides it when the anchor leaves. |
| variant | 'solid' | 'acrylic' | 'auto' | 'auto' · Own default, not ambient | The floating-surface paint (.jx-surface variants: solid, the 72%-alpha blur acrylic, auto = acrylic unless reduced transparency). Literal slot — own 'auto', the dialog/sheet exemplar's twin. |
| id | string | $props.id() | The panel id the declarative wire points at; derives the title/desc aria ids. |
| content | Snippet | — | Replaces the title/description area — the caller owns the semantics (aria-labelledby drops; wire your own ids). |
| actions | Snippet | — | Replaces the confirm/cancel row — close through the platform (a popovertarget button, light dismiss) or your own handler. |
| children * | Snippet | — | The trigger — any focusable control; the first button auto-wires as the declarative popovertarget trigger. |
| class / rest | string / HTMLAttributes | '' | The anchor span's lane — class joins the anchor atoms; rest spreads onto the wrapper (the trigger lane's host). The PANEL carries the family wiring and takes no spread. |
Universal props
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | the base scale — root font-size; parts size in em |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' · ambient scope | corner geometry (CSS corner-shape; §14 degrade table) |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | corner size; auto = the concentric broadcast (§3) |
| density | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' · ambient scope | spacing/leading scale over the kernel channels (§4) |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' · ambient scope | the hue axis of the fixed oklch primary system (§5) |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' · ambient scope | light/dark profile; system = the JS-mutable global (§6) |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'auto' · ambient scope | official M3 levels over the surface ladder (§7) |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' · ambient scope | intensity across the motion kernels (§8) |
axes
The eight axes on popconfirm
Resolved through PopconfirmDefaults (the single audited contract: variant's literal own 'auto' + elevation's own level2 over the eight universal slots). The PORTAL LAW: the anchor span and the promoted panel are siblings, so the carriers and the radius/elevation consumption stamps land on the PANEL itself — the promoted root is self-carried; the family's OWN sheet reads them (never popover.css). Density is the provider lane: the rung re-bases the panel's kernel channels AND re-tiers the trigger tenant through the anchor scope.
Reading the table: Property is the axis, Type is the real carrier or consumption it drives on THIS family, Default is the lane default — the named steps, number unit, and consumption are in each description.
| Property | Type | Default | Description |
|---|---|---|---|
| size | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | SUPPLY-ONLY for the content — the §11 stamp lands on the PANEL (the portal law: the promoted root is self-carried), but every voice on the panel declares its own type (the title's --text-label-lg, the body's --jx-text-base, the buttons' --jx-text) and beats inheritance by declaration. The one reached surface: the panel's own anonymous flow. Number unit: px. |
| shape | 'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto' | 'auto' | CONSUMED, SELF-CARRIED — the family's own sheet reads the carrier: popconfirm.css declares corner-shape: var(--jx-shape-effective, round) on .jx-pc, and the panel stamps the carrier in its own style (the portal law). No promotion-away needed — the stamped element IS the read element's root. |
| radius | 'small' | 'medium' | 'large' | 'auto' | number | 'auto' | CONSUMED, SELF-CARRIED — the panel always stamps --jx-radius-consumed (explicit: radius-effective × the §14 factor; auto: the §3 concentric calc against the panel's OWN ancestors) and .jx-pc's border-radius reads exactly that var (popconfirm.css, the same law verbatim as popover.css). Measured: the ambient page resolves the concentric calc to the square 0px; radius='large' on the component and the OPEN panel's computed radius jumps to the large rung. Number unit: px. |
| density | 'small' | 'medium' | 'large' | 'auto' | number (+ legacy spellings) | 'auto' | CONSUMED THROUGH THE KERNEL LANES, PROVIDED DOWN — the family is a density PROVIDER (inherit-then-provide, the button-group r11 lane): an explicit rung stamps data-density on BOTH the anchor span and the panel and re-bases the kernel channels inside each — the panel's title/body/buttons read var(--jx-hit/--jx-gap/--jx-inset/--jx-stack/--jx-text/--jx-line) (raw var() strings: substitution at the consuming element), and the TRIGGER tenant re-tiers with the anchor (a PressButton under a density='lg' anchor measures 48px against 40px beside it — measured). No opinion → no attribute anywhere (the density-adoption fleet law pins this family). Number unit: coefficient — carrier-stamped, declaration-scope-frozen for the lanes (the badge-indicator law). |
| color | 'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string | 'auto' | SUPPLY-ONLY — stamps --jx-color-effective on the panel; zero atom reads (grep receipt). The bubble's hues are idiomatic: the destructive confirm by default, primary on opt-in (confirmTone), the title/body ink the theme token law. Number unit: hue degrees. |
| theme | 'light' | 'dark' | 'system' | 'auto' | 'auto' | THE SPLIT (per-voice, measured): the class:dark bridge lands on the PANEL and the SURFACE flips — .jx-surface's fill chain reads the level-table rung then the RAW --popover, both re-declared under .dark, so the bubble re-schemes with the promotion intact (the panel is the dark scope; its content inherits it). The ATOM voices freeze — title/body/buttons read the typed --jx-foreground/--jx-muted-foreground/--jx-destructive/--jx-primary aliases, computed once at :root. Measured: the acrylic surface drops from oklch(0.96 / 72%) to oklch(0.185 / 77%) while the title ink holds its light black — the frozen pole, one promotion from the badge-indicator's all-frozen chip; the difference is the surface's raw reads. |
| elevation | 'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number | 'level2' · Own default, not ambient | CONSUMED, SELF-CARRIED — the family's OWN (level2, 3dp: the anchored menu rung) resolves even when the prop is auto, and the §7 pair + solid-fill bridge stamp ON THE PANEL; .jx-surface's fill chain reads the rung first (var(--jx-elevation-surface, var(--jx-surface-solid-fill, var(--popover)))). Measured: level2 vs elevation='level3' changes the OPEN panel's surface fill. The shadow layer rides the same pair (the kernel animates it in lockstep). Number unit: dp. |
| motion | 'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number | 'auto' | SUPPLY-ONLY — stamps --jx-motion-effective; zero atom reads (grep receipt). The real motion is the shared WAAPI surface-motion kernel (the toggle seam drives the --jx-p timeline; the live panel↔anchor axis measures every open) with its own prefers-reduced-motion guard — LAW #14's measured consequence: end states only settle past the kernel's duration, so probes await before asserting. Number unit: coefficient. |
| Token | Default | Source |
|---|---|---|
| --jx-hit / --jx-line | density scale ([data-density] scopes) | density |
| --jx-gap / --jx-inset / --jx-stack | density scale | density |
| --jx-text / --jx-text-base | kernel channels | density |
| --jx-pc-gap | 8px | component |
| --track-10 / --leading-15 | 0.1em / 1.5 (promotion seams, reported) | structural |
| --jx-shadow-xs | theme token | structural |
popconfirm · the measured axes
ambient — level2 own, concentric radius
Delete this row?
The history goes with it.
radius="large" — self-carried consumption
Delete this row?
elevation="level3" — the surface rung moves
Publish now?
theme="dark" — the surface flips, the ink freezes
Sure?
density="lg" — the provider re-tiers trigger and panel
Delete this row?
placement="bottom" — position-area + flip fallbacks
Delete this row?
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import PressButton from '@ui/press-button.svelte';
</script>
<div class={cx(rt.gridSm2, rt.wFull)}>
<div class={cx(rt.panel)} data-probe="pc-ambient">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>ambient — level2 own, concentric radius</p>
<Popconfirm id="pc-ax-ambient" title="Delete this row?" description="The history goes with it.">
<PressButton popovertarget="pc-ax-ambient">delete</PressButton>
</Popconfirm>
</div>
<div class={cx(rt.panel)} data-probe="pc-radius">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>radius="large" — self-carried consumption</p>
<Popconfirm id="pc-ax-radius" title="Delete this row?" radius="large">
<PressButton popovertarget="pc-ax-radius">delete</PressButton>
</Popconfirm>
</div>
<div class={cx(rt.panel)} data-probe="pc-elev">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>elevation="level3" — the surface rung moves</p>
<Popconfirm id="pc-ax-elev" title="Publish now?" confirmTone="primary" elevation="level3">
<PressButton popovertarget="pc-ax-elev">publish</PressButton>
</Popconfirm>
</div>
<div class={cx(rt.panel)} data-probe="pc-dark">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>theme="dark" — the surface flips, the ink freezes</p>
<Popconfirm id="pc-ax-dark" title="Sure?" theme="dark">
<PressButton popovertarget="pc-ax-dark">delete</PressButton>
</Popconfirm>
</div>
<div class={cx(rt.panel)} data-probe="pc-density">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>density="lg" — the provider re-tiers trigger and panel</p>
<Popconfirm id="pc-ax-density" title="Delete this row?" density="lg">
<PressButton popovertarget="pc-ax-density">delete</PressButton>
</Popconfirm>
</div>
<div class={cx(rt.panel)} data-probe="pc-placement">
<p class={cx(rt.eyebrow, rt.mb12, rt.inkMuted)}>placement="bottom" — position-area + flip fallbacks</p>
<Popconfirm id="pc-ax-placement" title="Delete this row?" placement="bottom">
<PressButton popovertarget="pc-ax-placement">delete</PressButton>
</Popconfirm>
</div>
</div><script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the bubble rides the ambient scope stamped 'sm'; from
md up the provider states its own lg opinion (both the anchor and
the panel stamp it; the trigger tenant re-tiers with the anchor) -->
<Popconfirm id="pc-query" title="Delete this row?" density={query({ md: 'lg' }, 'sm')}>
<PressButton popovertarget="pc-query">delete row</PressButton>
</Popconfirm>popconfirm · query()
Delete this row?
<script lang="ts">
import Popconfirm from '@ui/popconfirm.svelte';
import { query } from '@lib/universal-props-query.svelte';
</script>
<!-- below 48rem the bubble rides the ambient scope stamped 'sm'; from
md up the provider states its own lg opinion (both the anchor and
the panel stamp it; the trigger tenant re-tiers with the anchor) -->
<Popconfirm id="pc-query" title="Delete this row?" density={query({ md: 'lg' }, 'sm')}>
<PressButton popovertarget="pc-query">delete row</PressButton>
</Popconfirm>a11y
Accessibility
A compact dialog-like popover puts the safe cancel action first and treats every light dismissal as cancel; a content override owns its own semantics.
Keyboard
| Key | Action |
|---|---|
| Tab | Move between Cancel and Confirm (the two-button row in the default rendering) |
| Escape | Cancel and close — the native light-dismiss path, wired to oncancel |
| open | Focus lands on CANCEL (the safe action — the system-dialog law in its light form); the destructive path is never the landing spot |
ARIA
| Attribute | Value | Description |
|---|---|---|
| role | dialog | The confirmation surface — a light dialog on a popover=auto platform layer, never an alertdialog |
| aria-labelledby | {id}-title | Names the default title content; DROPS when a content snippet renders (the caller wires their own ids) |
| aria-describedby | {id}-desc | References the optional description — default rendering only |
| aria-expanded / aria-controls | mirrored on the trigger | The wrapper auto-wires the first button as the declarative popovertarget trigger and mirrors the live state every run |
| no aria-live | ruling | The outcome is the CALLER'S event — onconfirm/oncancel are handlers, not announcements; compose a live region around your own flow when the change must be spoken |