Slide to confirm, a satisfying UI interaction
For the one action that should take a moment: sending money, deleting a project, going live. Drag the knob to the end to confirm.
UI loop
Slide to confirm
Transfer
120.00
to Maya, savings pot
Slide to send
Sundry Millsundrymill.com/library
Everything in the preview is live. Try it.
The code
Slide to confirm
Drag the knob to the end. Keyboard users press Enter or Space on it.
HTML
<div class="stc" data-stc>
<span class="stc-label">Slide to send</span>
<button class="stc-knob" type="button" aria-label="Slide to send. Or press Enter.">
<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 12h14M13 6l6 6-6 6"/></svg>
</button>
</div>
CSS
.stc {
--pad: 5px;
--knob: 52px;
position: relative;
width: min(100%, 300px);
height: calc(var(--knob) + var(--pad) * 2);
border-radius: 999px;
background: #17202C;
color: #FDF9F1;
display: grid;
place-items: center;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
touch-action: none;
}
.stc::before {
content: "";
position: absolute;
inset: 0 auto 0 0;
width: calc(var(--x, 0px) + var(--knob) + var(--pad) * 2);
border-radius: inherit;
background: #FF7A2F;
}
.stc-label {
position: relative;
padding-left: calc(var(--knob) * .6);
font-weight: 700;
opacity: calc(1 - var(--p, 0) * 1.6);
}
.stc-knob {
position: absolute;
left: var(--pad);
top: var(--pad);
width: var(--knob);
height: var(--knob);
display: grid;
place-items: center;
border: 0;
border-radius: 50%;
background: #FDF9F1;
color: #17202C;
cursor: grab;
transform: translateX(var(--x, 0px));
}
.stc-knob svg { width: 22px; height: 22px; fill: none; stroke: currentColor; stroke-width: 2.4; stroke-linecap: round; stroke-linejoin: round; }
.stc-knob:active { cursor: grabbing; }
.stc.is-back::before { transition: width .45s cubic-bezier(.05, .7, .1, 1); }
.stc.is-back .stc-knob { transition: transform .45s cubic-bezier(.05, .7, .1, 1); }
.stc.is-done, .stc.is-done::before { background: #1E8A50; }
.stc.is-done .stc-label { opacity: 1; padding-left: 0; padding-right: calc(var(--knob) * .6); }
JavaScript
document.querySelectorAll('[data-stc]').forEach((el) => {
const knob = el.querySelector('.stc-knob');
const label = el.querySelector('.stc-label');
const text = label.textContent;
let startX = 0;
let x = 0;
let dragging = false;
const max = () => el.clientWidth - knob.offsetWidth - knob.offsetLeft * 2;
const set = (v) => {
x = v;
el.style.setProperty('--x', v + 'px');
el.style.setProperty('--p', v / max());
};
const reset = () => {
el.classList.add('is-back');
el.classList.remove('is-done');
label.textContent = text;
set(0);
};
const done = () => {
el.classList.add('is-back', 'is-done');
set(max());
label.textContent = 'Sent';
el.dispatchEvent(new CustomEvent('confirm', { bubbles: true }));
setTimeout(reset, 2400); // for the demo; drop this in your app
};
knob.addEventListener('pointerdown', (e) => {
if (el.classList.contains('is-done')) return;
dragging = true;
knob.setPointerCapture(e.pointerId);
el.classList.remove('is-back');
startX = e.clientX - x;
});
knob.addEventListener('pointermove', (e) => {
if (dragging) set(Math.min(max(), Math.max(0, e.clientX - startX)));
});
const release = () => {
if (!dragging) return;
dragging = false;
if (x > max() * 0.9) done();
else reset();
};
knob.addEventListener('pointerup', release);
knob.addEventListener('pointercancel', release);
// Enter or Space on the knob (a click with no pointer) confirms straight away
knob.addEventListener('click', (e) => {
if (e.detail === 0 && !el.classList.contains('is-done')) done();
});
});
How to use it
- Use it for one important action per screen. A slider on every button turns a safeguard into a chore.
- The knob is a real button, so Tab reaches it and Enter or Space confirms. Keep that path: not everyone can drag.
- Listen for the confirm event on the slider to run your action, and remove the demo reset in the script.
Showing an interaction like this to a client or in a tutorial? Framok records your screen and zooms in on every click for you.
Free to use in personal and commercial work. No credit needed, though a link back is always nice.