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
Slide to confirm, a satisfying UI interaction

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.

More from the library

See the whole library