Button Library · Signature
Hold to Confirm
Press and keep holding while it fills, let go to cancel.
A plain click does nothing. Listen for the confirm event, it fires once the action is complete.
States
Default
Hover
Active
Focus
Code
sig-hold.html
<button class="btn-sig-hold" type="button" data-done="Deleted"> <span data-label>Hold to delete</span> </button>
sig-hold.css
.btn-sig-hold {
--btn-bg: #e5484d;
--btn-fg: #ffffff;
--btn-font: inherit;
--btn-size: 15px;
--btn-weight: 600;
--btn-px: 22px;
--btn-py: 11px;
--btn-w: auto;
--btn-h: auto;
--btn-bw: 1.5px;
--btn-line: #e5484d;
--btn-radius: 10px;
--btn-shadow: none;
--btn-speed: 180ms;
}
.btn-sig-hold {
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) 18%, #14141c);
border: var(--btn-bw) solid var(--btn-line);
border-radius: var(--btn-radius);
box-shadow: var(--btn-shadow);
cursor: pointer;
text-decoration: none;
white-space: nowrap;
transition: all var(--btn-speed) ease;
overflow: hidden;
isolation: isolate;
touch-action: none;
user-select: none;
}
.btn-sig-hold:focus-visible {
outline: 2px solid var(--btn-bg);
outline-offset: 2px;
}
.btn-sig-hold:disabled {
opacity: 0.5;
pointer-events: none;
}
.btn-sig-hold__icon {
width: 1.15em;
height: 1.15em;
flex-shrink: 0;
}
.btn-sig-hold::before {
content: "";
position: absolute;
inset: 0;
z-index: -1;
background: var(--btn-bg);
transform: scaleX(var(--filled, 0));
transform-origin: 0 50%;
transition: transform 200ms ease;
}
.btn-sig-hold[data-state=holding] {
--filled: 1;
}
.btn-sig-hold[data-state=holding]::before {
transition: transform 1200ms linear;
}
.btn-sig-hold[data-state=done] {
--filled: 1;
border-color: #16a34a;
}
.btn-sig-hold[data-state=done]::before {
background: #16a34a;
}
@media (prefers-reduced-motion: reduce) {
.btn-sig-hold,
.btn-sig-hold::before {
animation: none !important;
transition: none !important;
}
}
sig-hold.js
document.querySelectorAll(".btn-sig-hold").forEach((el) => {
const label = el.querySelector("[data-label]");
const idle = label.textContent;
let timer = 0;
const reset = () => {
el.removeAttribute("data-state");
label.textContent = idle;
};
const finish = () => {
el.dataset.state = "done";
label.textContent = el.dataset.done;
el.dispatchEvent(new CustomEvent("confirm", { bubbles: true }));
timer = setTimeout(reset, 1800);
};
const start = () => {
if (el.dataset.state) return;
el.dataset.state = "holding";
timer = setTimeout(finish, 1200);
};
const stop = () => {
if (el.dataset.state !== "holding") return;
clearTimeout(timer);
reset();
};
const keyDown = (e) => {
if (e.key !== " " && e.key !== "Enter") return;
e.preventDefault();
if (!e.repeat) start();
};
const keyUp = (e) => {
if (e.key === " " || e.key === "Enter") stop();
};
const ends = ["pointerup", "pointerleave", "pointercancel", "blur"];
el.addEventListener("pointerdown", start);
ends.forEach((name) => el.addEventListener(name, stop));
el.addEventListener("keydown", keyDown);
el.addEventListener("keyup", keyUp);
return () => {
clearTimeout(timer);
el.removeEventListener("pointerdown", start);
ends.forEach((name) => el.removeEventListener(name, stop));
el.removeEventListener("keydown", keyDown);
el.removeEventListener("keyup", keyUp);
};
});
sig-hold.tsx
import { useEffect, useRef } from "react";
import type React from "react";
import "./sig-hold.css"; // the CSS tab
function attach(el: HTMLElement) {
const label = el.querySelector("[data-label]") as HTMLElement;
const idle = label.textContent;
let timer = 0;
const reset = () => {
el.removeAttribute("data-state");
label.textContent = idle;
};
const finish = () => {
el.dataset.state = "done";
label.textContent = el.dataset.done;
el.dispatchEvent(new CustomEvent("confirm", { bubbles: true }));
timer = setTimeout(reset, 1800);
};
const start = () => {
if (el.dataset.state) return;
el.dataset.state = "holding";
timer = setTimeout(finish, 1200);
};
const stop = () => {
if (el.dataset.state !== "holding") return;
clearTimeout(timer);
reset();
};
const keyDown = (e: KeyboardEvent) => {
if (e.key !== " " && e.key !== "Enter") return;
e.preventDefault();
if (!e.repeat) start();
};
const keyUp = (e: KeyboardEvent) => {
if (e.key === " " || e.key === "Enter") stop();
};
const ends = ["pointerup", "pointerleave", "pointercancel", "blur"];
el.addEventListener("pointerdown", start);
ends.forEach((name) => el.addEventListener(name, stop));
el.addEventListener("keydown", keyDown);
el.addEventListener("keyup", keyUp);
return () => {
clearTimeout(timer);
el.removeEventListener("pointerdown", start);
ends.forEach((name) => el.removeEventListener(name, stop));
el.removeEventListener("keydown", keyDown);
el.removeEventListener("keyup", keyUp);
};
}
export function BtnSigHold({ children = "Hold to delete", ...props }: React.ComponentProps<"button">) {
const ref = useRef<HTMLButtonElement>(null);
useEffect(() => attach(ref.current!), []);
return (
<button className="btn-sig-hold" type="button" data-done="Deleted" ref={ref} {...props}>
<span data-label>{children}</span>
</button>
);
}
sig-hold.html
<button class="btn-sig-hold relative inline-flex items-center justify-center gap-[0.5em] [box-sizing:border-box] [padding:11px_22px] [font-family:inherit] text-[15px] font-[600] [line-height:1.2] text-[#ffffff] [background:color-mix(in_srgb,_#e5484d_18%,_#14141c)] [border:1.5px_solid_#e5484d] rounded-[10px] [box-shadow:none] cursor-pointer no-underline whitespace-nowrap [transition:all_180ms_ease] overflow-hidden isolate [touch-action:none] [user-select:none] focus-visible:[outline:2px_solid_#e5484d] focus-visible:[outline-offset:2px] disabled:[opacity:0.5] disabled:[pointer-events:none] before:content-[''] before:absolute before:[inset:0] before:[z-index:-1] before:bg-[#e5484d] before:[transform:scaleX(var(--filled,_0))] before:[transform-origin:0_50%] before:[transition:transform_200ms_ease] [&[data-state=holding]]:[--filled:1] [&[data-state=holding]]:before:[transition:transform_1200ms_linear] [&[data-state=done]]:[--filled:1] [&[data-state=done]]:[border-color:#16a34a] [&[data-state=done]]:before:bg-[#16a34a] motion-reduce:[transition:none!important] motion-reduce:before:[transition:none!important]" type="button" data-done="Deleted"> <span data-label>Hold to delete</span> </button> <!-- Needs the script from the JS tab. -->