Buttons & actions
Drag to Confirm
Spring-resistant slider for confirming destructive or critical operations
detent-drag-to-confirm
Port status
- Catalog status
- implemented
- Origin
- upstream
- Phase
- 4
- Wave
- phase-4-motion-controls
- Group
- Buttons & actions
7.8 KB gzip for the class module · inlines the shared spring
Variants
Local demo. Drag a handle past 85% of the track, or focus it and press
Space, Enter or ArrowRight, to confirm. Releasing early snaps the handle back with
the pinned spring. The slider above and the first two on this page auto-reset after 2.5 s and report
detent-reset; the next is controlled by is-confirmed with the timer disabled; the last is
disabled. Each confirmation reports detent-confirm with an empty detail and pulses the device where
vibration is available — nothing is deleted, unlocked, submitted or archived.
No confirmation yet.
Recorded states
focus-visiblerecorded state; no setterdisabledrecorded state; no setterdraggingrecorded state; no setterdark themerecorded state; no setterreduced motionrecorded state; no setter
Usage
Import
import '/components/drag-to-confirm/define.js';
// or register it yourself
import { DetentDragToConfirm } from '/components/drag-to-confirm/drag-to-confirm.js';
Attributes
action-typeauto-reset-delayconfirmed-labeldisabledhapticis-confirmedlabelsizevariant
Properties
onConfirmonReset
Slots
confirmed-iconicon
Events
detent-confirm— {}detent-reset— {}
Example
Runnable example: site/examples/drag-to-confirm.ts. The snippet below is generated from the catalogued attributes.
<detent-drag-to-confirm
action-type="confirm"
auto-reset-delay="2500"
confirmed-label="Confirmed ✓"
disabled
haptic
is-confirmed
label="Slide to confirm"
size="md"
variant
></detent-drag-to-confirm>
Property-only values
const element = document.querySelector('detent-drag-to-confirm');
if (element !== null) {
// onConfirm: () => void
element.onConfirm = () => {};
// onReset: () => void
element.onReset = () => {};
}
Source
The pinned upstream source for this row:
src/components/ui/DragToConfirm.tsxsha256 55f59fe6a48d…
Native files
The native module and its artifacts:
src/components/drag-to-confirm/drag-to-confirm.tssrc/components/drag-to-confirm/styles.csssrc/components/drag-to-confirm/define.tsdocs/components/drag-to-confirm.mdsite/examples/drag-to-confirm.tstests/browser/drag-to-confirm.spec.tsevidence/components/drag-to-confirm.md
API
Boolean attributes follow HTML presence semantics: present means true, absent means false, and disabled="false" still means disabled.
Attributes
| Attribute | Property | Type | Default | Notes |
|---|---|---|---|---|
action-type | actionType | 'delete' | 'archive' | 'confirm' | 'submit' | 'unlock' | 'continue' | 'confirm' | Kebab-case attribute; camelCase property. |
auto-reset-delay | autoResetDelay | number | 2500 | Kebab-case attribute; camelCase property. |
confirmed-label | confirmedLabel | string | 'Confirmed ✓' | Kebab-case attribute; camelCase property. |
disabled | disabled | boolean | false | Presence semantics: present means true, absent means false. |
haptic | haptic | boolean | true | Presence semantics: present means true, absent means false. |
is-confirmed | isConfirmed | boolean | undefined | Presence semantics: present means true, absent means false. |
label | label | string | 'Slide to confirm' | Kebab-case attribute; camelCase property. |
size | size | 'sm' | 'md' | 'lg' | 'md' | Kebab-case attribute; camelCase property. |
variant | variant | 'default' | 'danger' | 'warning' | 'info' | 'success' | undefined | Kebab-case attribute; camelCase property. |
Properties
| Property | Type | Default | Notes |
|---|---|---|---|
onConfirm | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
onReset | () => void | undefined | Property-only: assign from script; not accepted as an attribute. |
Events
| Event | Upstream prop | Payload type | Notes |
|---|---|---|---|
detent-confirm | onConfirm | () => void | Composed, bubbles; fires only for a user action. |
detent-reset | onReset | () => void | Composed, bubbles; fires only for a user action. |
Slots
| Slot | Notes |
|---|---|
confirmed-icon | Consumer content; rendered through a slot, never re-created. |
icon | Consumer content; rendered through a slot, never re-created. |
CSS customisation
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The spring retarget is position-continuous but velocity restarts at zero because the shared helper starts at rest; springSnappy 380/30/0.5 for snapback and springResponsive 320/24/0.7 for reset and controlled return are kept.
- The fallback action button becomes visible while focused (upstream clipped it permanently), an accessibility correction.
- Boundary, fill and tone colours derive from theme tokens so both schemes reach the WCAG 1.4.11 non-text ratio.
- disabled also cancels an in-flight gesture and returns the handle to rest.
- A controlled is-confirmed slider emits detent-confirm but does not move, and the auto-reset timer still emits detent-reset.
- The disabled state keeps its label at full text contrast and carries the inactive look in the chrome instead: the pill boundary falls back to the border token, the handle loses its shadow, its glyph is mixed toward the handle surface and the handle alone is at 60% opacity. Fading the whole pill to 40% had painted the label at 1.92:1 light and 2.56:1 dark.
What it does
A pill-shaped slider you drag to the right to confirm a destructive or critical action, with a progressive fill, a spring snapback on incomplete drags, an optional auto-reset timer and an optional haptic pulse.
Upstream behaviour
- Clean minimalist pill geometry with seamless light and dark mode theme compatibility
- Dynamic progressive track fill that physically tracks the handle as you drag
- Physical gesture drag handle with calibrated spring snapback upon incomplete drags
- Supports Delete, Archive, Confirm, Submit, and Unlock action profiles with custom sizes (sm, md, lg)
- Automatic post-confirmation reset timer with customizable delay or controlled state support
- Full keyboard accessibility (Space, Enter, ArrowRight to trigger) and mobile haptic vibration feedback
Differences from upstream
- React `children`/`ReactNode` props become DOM slots; caller content stays light DOM.
- React callback props become typed composed CustomEvents; programmatic state assignment emits none.
- Framer Motion springs/AnimatePresence ported to CSS transitions, WAAPI or the shared rAF spring helper.
- `lucide-react` icon components replaced with local SVG assets (ISC notice retained).
- React state/hooks replaced by element-owned state with pre-upgrade property preservation.
- The spring retarget is position-continuous but velocity restarts at zero because the shared helper starts at rest; springSnappy 380/30/0.5 for snapback and springResponsive 320/24/0.7 for reset and controlled return are kept.
- The fallback action button becomes visible while focused (upstream clipped it permanently), an accessibility correction.
- Boundary, fill and tone colours derive from theme tokens so both schemes reach the WCAG 1.4.11 non-text ratio.
- disabled also cancels an in-flight gesture and returns the handle to rest.
- A controlled is-confirmed slider emits detent-confirm but does not move, and the auto-reset timer still emits detent-reset.
- The disabled state keeps its label at full text contrast and carries the inactive look in the chrome instead: the pill boundary falls back to the border token, the handle loses its shadow, its glyph is mixed toward the handle surface and the handle alone is at 60% opacity. Fading the whole pill to 40% had painted the label at 1.92:1 light and 2.56:1 dark.