Button Library · Signature
Slide to Confirm
Drag the knob all the way, a click cannot trigger it.
A plain click does nothing. Listen for the confirm event, it fires once the action is complete.
States
Default
Hover
Active
Focus
Code
sig-slide.html
<button class="btn-sig-slide" type="button" data-done="Paid">
<span class="btn-sig-slide__fill" aria-hidden="true"></span>
<span class="btn-sig-slide__word" data-label>Slide to pay</span>
<span class="btn-sig-slide__thumb" data-thumb aria-hidden="true">
<svg class="btn-sig-slide__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14"></path>
<path d="m13 6 6 6-6 6"></path>
</svg>
</span>
</button>
sig-slide.css
.btn-sig-slide {
--btn-bg: #7c5cff;
--btn-fg: #ffffff;
--btn-font: inherit;
--btn-size: 15px;
--btn-weight: 600;
--btn-px: 0px;
--btn-py: 0px;
--btn-w: 260px;
--btn-h: 52px;
--btn-bw: 0px;
--btn-line: #8b6cff;
--btn-radius: 26px;
--btn-shadow: none;
--btn-speed: 180ms;
}
.btn-sig-slide {
position: relative;
display: inline-flex;
align-items: center;
justify-content: center;
gap: 0.5em;
box-sizing: border-box;
width: var(--btn-w);
height: var(--btn-h);
padding: var(--btn-py) var(--btn-px);
font-family: var(--btn-font);
font-size: var(--btn-size);
font-weight: var(--btn-weight);
line-height: 1.2;
color: var(--btn-fg);
background: color-mix(in srgb, var(--btn-bg) 22%, #14141c);
border: var(--btn-bw) solid var(--btn-line);
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
cursor: grab;
text-decoration: none;
white-space: nowrap;
transition: all var(--btn-speed) ease;
overflow: hidden;
min-width: 200px;
touch-action: none;
user-select: none;
}
.btn-sig-slide:focus-visible {
outline: 2px solid var(--btn-bg);
outline-offset: 2px;
}
.btn-sig-slide:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-sig-slide__icon {
width: 1.15em;
height: 1.15em;
flex-shrink: 0;
}
.btn-sig-slide[data-state=drag] {
--glide: 0ms;
cursor: grabbing;
}
.btn-sig-slide[data-state=done] {
--shade: #16a34a;
cursor: default;
}
.btn-sig-slide__fill {
position: absolute;
top: 0;
bottom: 0;
left: 0;
width: calc(var(--x, 0px) + 48px);
background: var(--shade, var(--btn-bg));
border-radius: inherit;
transition: width var(--glide, 220ms) ease, background 220ms ease;
}
.btn-sig-slide__word {
position: relative;
padding-left: 36px;
opacity: calc(1 - var(--p, 0) * 1.3);
}
.btn-sig-slide__thumb {
position: absolute;
top: 4px;
bottom: 4px;
left: 4px;
display: grid;
place-items: center;
aspect-ratio: 1;
color: var(--shade, var(--btn-bg));
background: #ffffff;
border-radius: inherit;
transform: translateX(var(--x, 0px));
transition: transform var(--glide, 220ms) ease;
}
@media (prefers-reduced-motion: reduce) {
.btn-sig-slide,
.btn-sig-slide__fill,
.btn-sig-slide__thumb {
animation: none !important;
transition: none !important;
}
}
sig-slide.js
document.querySelectorAll(".btn-sig-slide").forEach((el) => {
const thumb = el.querySelector("[data-thumb]");
const label = el.querySelector("[data-label]");
const idle = label.textContent;
let timer = 0;
let from = -1;
const reach = () => el.clientWidth - thumb.offsetWidth - 8;
const place = (x) => {
el.style.setProperty("--x", x + "px");
el.style.setProperty("--p", String(x / reach()));
};
const reset = () => {
el.removeAttribute("data-state");
label.textContent = idle;
place(0);
};
const finish = () => {
el.dataset.state = "done";
label.textContent = el.dataset.done;
place(reach());
el.style.setProperty("--p", "0");
el.dispatchEvent(new CustomEvent("confirm", { bubbles: true }));
timer = setTimeout(reset, 1800);
};
const down = (e) => {
if (el.dataset.state) return;
from = e.clientX;
el.setPointerCapture(e.pointerId);
el.dataset.state = "drag";
};
const move = (e) => {
if (from < 0) return;
const x = Math.min(Math.max(e.clientX - from, 0), reach());
place(x);
if (x >= reach()) {
from = -1;
finish();
}
};
const up = () => {
if (from < 0) return;
from = -1;
reset();
};
const key = (e) => {
if (e.detail === 0 && !el.dataset.state) finish();
};
el.addEventListener("pointerdown", down);
el.addEventListener("pointermove", move);
el.addEventListener("pointerup", up);
el.addEventListener("pointercancel", up);
el.addEventListener("click", key);
return () => {
clearTimeout(timer);
el.removeEventListener("pointerdown", down);
el.removeEventListener("pointermove", move);
el.removeEventListener("pointerup", up);
el.removeEventListener("pointercancel", up);
el.removeEventListener("click", key);
};
});
sig-slide.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./sig-slide.css"; // the CSS tab
function attach(el: HTMLElement) {
const thumb = el.querySelector("[data-thumb]") as HTMLElement;
const label = el.querySelector("[data-label]") as HTMLElement;
const idle = label.textContent;
let timer = 0;
let from = -1;
const reach = () => el.clientWidth - thumb.offsetWidth - 8;
const place = (x: number) => {
el.style.setProperty("--x", x + "px");
el.style.setProperty("--p", String(x / reach()));
};
const reset = () => {
el.removeAttribute("data-state");
label.textContent = idle;
place(0);
};
const finish = () => {
el.dataset.state = "done";
label.textContent = el.dataset.done;
place(reach());
el.style.setProperty("--p", "0");
el.dispatchEvent(new CustomEvent("confirm", { bubbles: true }));
timer = setTimeout(reset, 1800);
};
const down = (e: PointerEvent) => {
if (el.dataset.state) return;
from = e.clientX;
el.setPointerCapture(e.pointerId);
el.dataset.state = "drag";
};
const move = (e: PointerEvent) => {
if (from < 0) return;
const x = Math.min(Math.max(e.clientX - from, 0), reach());
place(x);
if (x >= reach()) {
from = -1;
finish();
}
};
const up = () => {
if (from < 0) return;
from = -1;
reset();
};
const key = (e: MouseEvent) => {
if (e.detail === 0 && !el.dataset.state) finish();
};
el.addEventListener("pointerdown", down);
el.addEventListener("pointermove", move);
el.addEventListener("pointerup", up);
el.addEventListener("pointercancel", up);
el.addEventListener("click", key);
return () => {
clearTimeout(timer);
el.removeEventListener("pointerdown", down);
el.removeEventListener("pointermove", move);
el.removeEventListener("pointerup", up);
el.removeEventListener("pointercancel", up);
el.removeEventListener("click", key);
};
}
export function BtnSigSlide({ children = "Slide to pay", ...props }: React.ComponentProps<"button">) {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<button className="btn-sig-slide" type="button" data-done="Paid" ref={ref} {...props}>
<span className="btn-sig-slide__fill" aria-hidden="true" />
<span className="btn-sig-slide__word" data-label>{children}</span>
<span className="btn-sig-slide__thumb" data-thumb aria-hidden="true">
<svg className="btn-sig-slide__icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M5 12h14" />
<path d="m13 6 6 6-6 6" />
</svg>
</span>
</button>
);
}
sig-slide.html
<button class="btn-sig-slide relative inline-flex items-center justify-center gap-[0.5em] [box-sizing:border-box] w-[260px] h-[52px] [padding:0px_0px] [font-family:inherit] text-[15px] font-[600] [line-height:1.2] text-[#ffffff] [background:color-mix(in_srgb,_#7c5cff_22%,_#14141c)] [border:0px_solid_#8b6cff] rounded-[26px] [box-shadow:none] [cursor:grab] no-underline whitespace-nowrap [transition:all_180ms_ease] overflow-hidden [min-width:200px] [touch-action:none] [user-select:none] focus-visible:[outline:2px_solid_#7c5cff] focus-visible:[outline-offset:2px] disabled:[opacity:0.5] disabled:[pointer-events:none] [&[data-state=drag]]:[--glide:0ms] [&[data-state=drag]]:[cursor:grabbing] [&[data-state=done]]:[--shade:#16a34a] [&[data-state=done]]:[cursor:default] motion-reduce:[transition:none!important]" type="button" data-done="Paid">
<span class="absolute [top:0] [bottom:0] [left:0] w-[calc(var(--x,_0px)_+_48px)] [background:var(--shade,_#7c5cff)] rounded-[inherit] [transition:width_var(--glide,_220ms)_ease,_background_220ms_ease] motion-reduce:[transition:none!important]" aria-hidden="true"></span>
<span class="relative [padding-left:36px] [opacity:calc(1_-_var(--p,_0)_*_1.3)]" data-label>Slide to pay</span>
<span class="absolute [top:4px] [bottom:4px] [left:4px] grid [place-items:center] [aspect-ratio:1] [color:var(--shade,_#7c5cff)] bg-[#ffffff] rounded-[inherit] [transform:translateX(var(--x,_0px))] [transition:transform_var(--glide,_220ms)_ease] motion-reduce:[transition:none!important]" data-thumb aria-hidden="true">
<svg class="w-[1.15em] h-[1.15em] [flex-shrink:0]" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12h14"></path>
<path d="m13 6 6 6-6 6"></path>
</svg>
</span>
</button>
<!-- Needs the script from the JS tab. -->