jixoai-ui
the terminal design language
loading the style sheet…
Skip to content
docs · print medium: screen

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.

install — popconfirm

$npx jixoai-ui add popconfirm

https://ui.jixoai.com/r/popconfirm.json

role=dialog light dismiss = cancel focus → Cancel content / actions snippets

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.

popconfirm-usage.svelte svelte
<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.

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.

PropertyTypeDefaultDescription
open trigger click (declarative popovertarget)the anchored bubble — placement with flip fallbacksannounces: role="dialog" named by the title
confirm the confirm buttononconfirm 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 buttononcancel runs — ANY dismissal that is not confirm is a cancelannounces: nothing was destroyed
focus openfocus 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 snippetsthe caller owns the area AND the semantics — aria-labelledby drops (wire your own ids)announces: whatever the caller authors
usage
<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

popconfirm-types-demo.svelte svelte
<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.

Popconfirm usage
<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

PropertyTypeDefaultDescription
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 ambientThe 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

PropertyTypeDefaultDescription
size'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopethe base scale — root font-size; parts size in em
shape'round' | 'scoop' | 'bevel' | 'notch' | 'square' | 'squircle' | 'auto''auto' · ambient scopecorner geometry (CSS corner-shape; §14 degrade table)
radius'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopecorner size; auto = the concentric broadcast (§3)
density'small' | 'medium' | 'large' | 'auto' | number'auto' · ambient scopespacing/leading scale over the kernel channels (§4)
color'primary' | 'secondary' | 'error' | 'warn' | 'success' | 'info' | 'auto' | number | string'auto' · ambient scopethe hue axis of the fixed oklch primary system (§5)
theme'light' | 'dark' | 'system' | 'auto''auto' · ambient scopelight/dark profile; system = the JS-mutable global (§6)
elevation'level-1' | 'level0' | 'level1' | 'level2' | 'level3' | 'level4' | 'level5' | 'auto' | number'auto' · ambient scopeofficial M3 levels over the surface ladder (§7)
motion'reduced' | 'subtle' | 'normal' | 'expressive' | 'auto' | number'auto' · ambient scopeintensity 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.

PropertyTypeDefaultDescription
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 ambientCONSUMED, 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.
TokenDefaultSource
--jx-hit / --jx-linedensity scale ([data-density] scopes)density
--jx-gap / --jx-inset / --jx-stackdensity scaledensity
--jx-text / --jx-text-basekernel channelsdensity
--jx-pc-gap8pxcomponent
--track-10 / --leading-150.1em / 1.5 (promotion seams, reported)structural
--jx-shadow-xstheme tokenstructural

popconfirm · the measured axes

ambient — level2 own, concentric radius

radius="large" — self-carried consumption

elevation="level3" — the surface rung moves

theme="dark" — the surface flips, the ink freezes

density="lg" — the provider re-tiers trigger and panel

placement="bottom" — position-area + flip fallbacks

popconfirm-axes.svelte svelte
<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>
one real query() case
<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()

popconfirm-query-demo.svelte svelte
<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

KeyAction
TabMove between Cancel and Confirm (the two-button row in the default rendering)
EscapeCancel and close — the native light-dismiss path, wired to oncancel
openFocus lands on CANCEL (the safe action — the system-dialog law in its light form); the destructive path is never the landing spot

ARIA

AttributeValueDescription
roledialogThe confirmation surface — a light dialog on a popover=auto platform layer, never an alertdialog
aria-labelledby{id}-titleNames the default title content; DROPS when a content snippet renders (the caller wires their own ids)
aria-describedby{id}-descReferences the optional description — default rendering only
aria-expanded / aria-controlsmirrored on the triggerThe wrapper auto-wires the first button as the declarative popovertarget trigger and mirrors the live state every run
no aria-liverulingThe 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
jixoai-ui ui.jixoai.com the jixoai design language

Theme

Brand hue

0°

Auto cycle · 24h