28 CSS Close Buttons24 / 28
Hold to Confirm Close Button
Destructive-action protection: closing requires pressing and HOLDING for 900ms while a conic-gradient ring fills clockwise around the button. Release early and the ring rewinds; hold to completion and the dismissal fires. The fill is one CSS custom property animated by @property — JS only starts, cancels, and listens for completion.
Published Updated
The code
<section class="ccb-24" aria-label="Hold to confirm close button demo">
<div class="ccb-24__wrap">
<button class="ccb-24__close" id="ccb-24-btn" type="button" aria-label="Hold to close">
<span class="ccb-24__x" aria-hidden="true"></span>
</button>
<span class="ccb-24__hint" id="ccb-24-hint">Press & hold to close</span>
</div>
</section><section class="ccb-24" aria-label="Hold to confirm close button demo">
<div class="ccb-24__wrap">
<button class="ccb-24__close" id="ccb-24-btn" type="button" aria-label="Hold to close">
<span class="ccb-24__x" aria-hidden="true"></span>
</button>
<span class="ccb-24__hint" id="ccb-24-hint">Press & hold to close</span>
</div>
</section>@property --ccb-24-p {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.ccb-24,
.ccb-24 *,
.ccb-24 *::before,
.ccb-24 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-24 {
--accent: oklch(0.62 0.21 25);
--hold: 900ms;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.2 0.02 25),oklch(0.14 0.015 20));
}
.ccb-24 .ccb-24__wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.ccb-24 .ccb-24__close {
--ccb-24-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 76px;
height: 76px;
border: none;
border-radius: 50%;
background: oklch(0.25 0.03 25);
cursor: pointer;
transition: --ccb-24-p .3s ease-out,transform .15s ease;
box-shadow: 0 18px 40px -20px rgba(0,0,0,.9);
}
.ccb-24 .ccb-24__close::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
background: conic-gradient(var(--accent) var(--ccb-24-p),oklch(0.35 0.03 25 / .5) 0);
-webkit-mask: radial-gradient(circle,transparent 66%,#000 67%);
mask: radial-gradient(circle,transparent 66%,#000 67%);
}
.ccb-24 .ccb-24__close.is-holding {
--ccb-24-p: 100%;
transition: --ccb-24-p var(--hold) linear,transform .15s ease;
transform: scale(.96);
}
.ccb-24 .ccb-24__close.is-done {
background: var(--accent);
}
.ccb-24 .ccb-24__x {
position: relative;
width: 24px;
height: 24px;
}
.ccb-24 .ccb-24__x::before,
.ccb-24 .ccb-24__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 3px;
border-radius: 2px;
background: oklch(0.95 0.02 25);
}
.ccb-24 .ccb-24__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-24 .ccb-24__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
.ccb-24 .ccb-24__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 5px;
}
.ccb-24 .ccb-24__hint {
font-size: 12px;
letter-spacing: .04em;
color: oklch(0.65 0.03 25);
}
@media (prefers-reduced-motion: reduce) {
.ccb-24 .ccb-24__close {
transition: none;
}
}@property --ccb-24-p {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.ccb-24,
.ccb-24 *,
.ccb-24 *::before,
.ccb-24 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-24 {
--accent: oklch(0.62 0.21 25);
--hold: 900ms;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.2 0.02 25),oklch(0.14 0.015 20));
}
.ccb-24 .ccb-24__wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.ccb-24 .ccb-24__close {
--ccb-24-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 76px;
height: 76px;
border: none;
border-radius: 50%;
background: oklch(0.25 0.03 25);
cursor: pointer;
transition: --ccb-24-p .3s ease-out,transform .15s ease;
box-shadow: 0 18px 40px -20px rgba(0,0,0,.9);
}
.ccb-24 .ccb-24__close::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
background: conic-gradient(var(--accent) var(--ccb-24-p),oklch(0.35 0.03 25 / .5) 0);
-webkit-mask: radial-gradient(circle,transparent 66%,#000 67%);
mask: radial-gradient(circle,transparent 66%,#000 67%);
}
.ccb-24 .ccb-24__close.is-holding {
--ccb-24-p: 100%;
transition: --ccb-24-p var(--hold) linear,transform .15s ease;
transform: scale(.96);
}
.ccb-24 .ccb-24__close.is-done {
background: var(--accent);
}
.ccb-24 .ccb-24__x {
position: relative;
width: 24px;
height: 24px;
}
.ccb-24 .ccb-24__x::before,
.ccb-24 .ccb-24__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 3px;
border-radius: 2px;
background: oklch(0.95 0.02 25);
}
.ccb-24 .ccb-24__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-24 .ccb-24__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
.ccb-24 .ccb-24__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 5px;
}
.ccb-24 .ccb-24__hint {
font-size: 12px;
letter-spacing: .04em;
color: oklch(0.65 0.03 25);
}
@media (prefers-reduced-motion: reduce) {
.ccb-24 .ccb-24__close {
transition: none;
}
}(() => {
const btn = document.getElementById('ccb-24-btn'); if (!btn) return;
const hint = document.getElementById('ccb-24-hint');
const HOLD = 900; let timer = null;
const start = () => {
if (btn.classList.contains('is-done')) return;
btn.classList.add('is-holding');
timer = setTimeout(complete, HOLD); // completion by timer — robust even if transitionend is missed
};
const cancel = () => { clearTimeout(timer); btn.classList.remove('is-holding'); };
const complete = () => {
btn.classList.add('is-done'); btn.classList.remove('is-holding');
hint.textContent = 'Closed ✓ (click to reset)';
navigator.vibrate?.(10);
};
btn.addEventListener('pointerdown', start);
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointerleave', cancel);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); start(); } });
btn.addEventListener('keyup', (e) => { if (e.key === 'Enter' || e.key === ' ') cancel(); });
btn.addEventListener('click', () => { // reset for the demo
if (btn.classList.contains('is-done')) { btn.classList.remove('is-done'); hint.textContent = 'Press & hold to close'; }
});
})();(() => {
const btn = document.getElementById('ccb-24-btn'); if (!btn) return;
const hint = document.getElementById('ccb-24-hint');
const HOLD = 900; let timer = null;
const start = () => {
if (btn.classList.contains('is-done')) return;
btn.classList.add('is-holding');
timer = setTimeout(complete, HOLD); // completion by timer — robust even if transitionend is missed
};
const cancel = () => { clearTimeout(timer); btn.classList.remove('is-holding'); };
const complete = () => {
btn.classList.add('is-done'); btn.classList.remove('is-holding');
hint.textContent = 'Closed ✓ (click to reset)';
navigator.vibrate?.(10);
};
btn.addEventListener('pointerdown', start);
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointerleave', cancel);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); start(); } });
btn.addEventListener('keyup', (e) => { if (e.key === 'Enter' || e.key === ' ') cancel(); });
btn.addEventListener('click', () => { // reset for the demo
if (btn.classList.contains('is-done')) { btn.classList.remove('is-done'); hint.textContent = 'Press & hold to close'; }
});
})();Here's a working CSS Close Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Hold to Confirm Close Button
Source: https://codefronts.com/components/css-close-buttons/hold-to-confirm/
Destructive-action protection: closing requires pressing and HOLDING for 900ms while a conic-gradient ring fills clockwise around the button. Release early and the ring rewinds; hold to completion and the dismissal fires. The fill is one CSS custom property animated by @property — JS only starts, cancels, and listens for completion.
## HTML
```html
<section class="ccb-24" aria-label="Hold to confirm close button demo">
<div class="ccb-24__wrap">
<button class="ccb-24__close" id="ccb-24-btn" type="button" aria-label="Hold to close">
<span class="ccb-24__x" aria-hidden="true"></span>
</button>
<span class="ccb-24__hint" id="ccb-24-hint">Press & hold to close</span>
</div>
</section>
```
## CSS
```css
@property --ccb-24-p {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.ccb-24,
.ccb-24 *,
.ccb-24 *::before,
.ccb-24 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-24 {
--accent: oklch(0.62 0.21 25);
--hold: 900ms;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.2 0.02 25),oklch(0.14 0.015 20));
}
.ccb-24 .ccb-24__wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.ccb-24 .ccb-24__close {
--ccb-24-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 76px;
height: 76px;
border: none;
border-radius: 50%;
background: oklch(0.25 0.03 25);
cursor: pointer;
transition: --ccb-24-p .3s ease-out,transform .15s ease;
box-shadow: 0 18px 40px -20px rgba(0,0,0,.9);
}
.ccb-24 .ccb-24__close::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
background: conic-gradient(var(--accent) var(--ccb-24-p),oklch(0.35 0.03 25 / .5) 0);
-webkit-mask: radial-gradient(circle,transparent 66%,#000 67%);
mask: radial-gradient(circle,transparent 66%,#000 67%);
}
.ccb-24 .ccb-24__close.is-holding {
--ccb-24-p: 100%;
transition: --ccb-24-p var(--hold) linear,transform .15s ease;
transform: scale(.96);
}
.ccb-24 .ccb-24__close.is-done {
background: var(--accent);
}
.ccb-24 .ccb-24__x {
position: relative;
width: 24px;
height: 24px;
}
.ccb-24 .ccb-24__x::before,
.ccb-24 .ccb-24__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 3px;
border-radius: 2px;
background: oklch(0.95 0.02 25);
}
.ccb-24 .ccb-24__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-24 .ccb-24__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
.ccb-24 .ccb-24__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 5px;
}
.ccb-24 .ccb-24__hint {
font-size: 12px;
letter-spacing: .04em;
color: oklch(0.65 0.03 25);
}
@media (prefers-reduced-motion: reduce) {
.ccb-24 .ccb-24__close {
transition: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.getElementById('ccb-24-btn'); if (!btn) return;
const hint = document.getElementById('ccb-24-hint');
const HOLD = 900; let timer = null;
const start = () => {
if (btn.classList.contains('is-done')) return;
btn.classList.add('is-holding');
timer = setTimeout(complete, HOLD); // completion by timer — robust even if transitionend is missed
};
const cancel = () => { clearTimeout(timer); btn.classList.remove('is-holding'); };
const complete = () => {
btn.classList.add('is-done'); btn.classList.remove('is-holding');
hint.textContent = 'Closed ✓ (click to reset)';
navigator.vibrate?.(10);
};
btn.addEventListener('pointerdown', start);
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointerleave', cancel);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); start(); } });
btn.addEventListener('keyup', (e) => { if (e.key === 'Enter' || e.key === ' ') cancel(); });
btn.addEventListener('click', () => { // reset for the demo
if (btn.classList.contains('is-done')) { btn.classList.remove('is-done'); hint.textContent = 'Press & hold to close'; }
});
})();
```Here's a working CSS Close Button from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Hold to Confirm Close Button
Source: https://codefronts.com/components/css-close-buttons/hold-to-confirm/
Destructive-action protection: closing requires pressing and HOLDING for 900ms while a conic-gradient ring fills clockwise around the button. Release early and the ring rewinds; hold to completion and the dismissal fires. The fill is one CSS custom property animated by @property — JS only starts, cancels, and listens for completion.
## HTML
```html
<section class="ccb-24" aria-label="Hold to confirm close button demo">
<div class="ccb-24__wrap">
<button class="ccb-24__close" id="ccb-24-btn" type="button" aria-label="Hold to close">
<span class="ccb-24__x" aria-hidden="true"></span>
</button>
<span class="ccb-24__hint" id="ccb-24-hint">Press & hold to close</span>
</div>
</section>
```
## CSS
```css
@property --ccb-24-p {
syntax: "<percentage>";
inherits: false;
initial-value: 0%;
}
.ccb-24,
.ccb-24 *,
.ccb-24 *::before,
.ccb-24 *::after {
box-sizing: border-box;
margin: 0;
padding: 0;
}
.ccb-24 {
--accent: oklch(0.62 0.21 25);
--hold: 900ms;
font-family: 'Segoe UI',system-ui,sans-serif;
width: 100%;
min-height: 100vh;
display: grid;
place-items: center;
padding: 48px 24px;
background: linear-gradient(180deg,oklch(0.2 0.02 25),oklch(0.14 0.015 20));
}
.ccb-24 .ccb-24__wrap {
display: flex;
flex-direction: column;
align-items: center;
gap: 16px;
}
.ccb-24 .ccb-24__close {
--ccb-24-p: 0%;
position: relative;
display: grid;
place-items: center;
width: 76px;
height: 76px;
border: none;
border-radius: 50%;
background: oklch(0.25 0.03 25);
cursor: pointer;
transition: --ccb-24-p .3s ease-out,transform .15s ease;
box-shadow: 0 18px 40px -20px rgba(0,0,0,.9);
}
.ccb-24 .ccb-24__close::before {
content: "";
position: absolute;
inset: -6px;
border-radius: 50%;
background: conic-gradient(var(--accent) var(--ccb-24-p),oklch(0.35 0.03 25 / .5) 0);
-webkit-mask: radial-gradient(circle,transparent 66%,#000 67%);
mask: radial-gradient(circle,transparent 66%,#000 67%);
}
.ccb-24 .ccb-24__close.is-holding {
--ccb-24-p: 100%;
transition: --ccb-24-p var(--hold) linear,transform .15s ease;
transform: scale(.96);
}
.ccb-24 .ccb-24__close.is-done {
background: var(--accent);
}
.ccb-24 .ccb-24__x {
position: relative;
width: 24px;
height: 24px;
}
.ccb-24 .ccb-24__x::before,
.ccb-24 .ccb-24__x::after {
content: "";
position: absolute;
top: 50%;
left: 50%;
width: 24px;
height: 3px;
border-radius: 2px;
background: oklch(0.95 0.02 25);
}
.ccb-24 .ccb-24__x::before {
transform: translate(-50%,-50%) rotate(45deg);
}
.ccb-24 .ccb-24__x::after {
transform: translate(-50%,-50%) rotate(-45deg);
}
.ccb-24 .ccb-24__close:focus-visible {
outline: 3px solid var(--accent);
outline-offset: 5px;
}
.ccb-24 .ccb-24__hint {
font-size: 12px;
letter-spacing: .04em;
color: oklch(0.65 0.03 25);
}
@media (prefers-reduced-motion: reduce) {
.ccb-24 .ccb-24__close {
transition: none;
}
}
```
## JavaScript
```js
(() => {
const btn = document.getElementById('ccb-24-btn'); if (!btn) return;
const hint = document.getElementById('ccb-24-hint');
const HOLD = 900; let timer = null;
const start = () => {
if (btn.classList.contains('is-done')) return;
btn.classList.add('is-holding');
timer = setTimeout(complete, HOLD); // completion by timer — robust even if transitionend is missed
};
const cancel = () => { clearTimeout(timer); btn.classList.remove('is-holding'); };
const complete = () => {
btn.classList.add('is-done'); btn.classList.remove('is-holding');
hint.textContent = 'Closed ✓ (click to reset)';
navigator.vibrate?.(10);
};
btn.addEventListener('pointerdown', start);
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointerleave', cancel);
btn.addEventListener('keydown', (e) => { if ((e.key === 'Enter' || e.key === ' ') && !e.repeat) { e.preventDefault(); start(); } });
btn.addEventListener('keyup', (e) => { if (e.key === 'Enter' || e.key === ' ') cancel(); });
btn.addEventListener('click', () => { // reset for the demo
if (btn.classList.contains('is-done')) { btn.classList.remove('is-done'); hint.textContent = 'Press & hold to close'; }
});
})();
```How this works
The ring is the button's border area painted by background:conic-gradient(accent var(--p), track 0) masked to a ring. @property --p registers the percentage as an animatable type, so CSS transitions it smoothly: holding adds a class that sets --p:100% with a linear 900ms transition; releasing removes the class and the ring rewinds at 3× speed (a shorter reverse transition).
JS handles intent, not animation: pointerdown starts the hold, pointerup/pointerleave cancels, and a transitionend listener on the custom property fires the actual close only when the fill reaches 100%. Keyboard users hold Space/Enter via keydown/keyup — the same path. The button's aria-label says "Hold to close" up front, and the visible hint flips to a confirmation on completion.
Make it yours
- Hold duration via
--hold(mirrored in the JS constant — keep them in sync). - Rewind speed via the base transition (300ms = forgiving, instant = strict).
- Ring thickness via the mask's inner radius.
- Fire a haptic on supported devices with navigator.vibrate(10) at completion.
Gotchas — read before shipping
- Without @property registration the conic fill SNAPS instead of sweeping — the whole effect depends on it (Firefox 128+).
- Cancel on pointerleave too, not just pointerup — dragging off a held button must abort the destruction.
- Never use hold-to-confirm for routine dismissals — reserve it for destructive, unrecoverable closes; friction on ordinary UI is hostile.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 115+ | 17+ | 128+ | 115+ |
@property is required for the smooth sweep (the interaction still works with a stepped fill on older engines).