DEMO
ボタンを1.2秒押し続けると削除します(キーボードは Space か Enter を押し続けます)
CODE
<div class="demo-stage htc-stage">
<button type="button" class="htc-btn" id="htc-btn" aria-describedby="htc-help">
<span class="htc-fill" id="htc-fill" aria-hidden="true"></span>
<span class="htc-label" id="htc-label">長押しで削除</span>
</button>
<p class="htc-help" id="htc-help">ボタンを1.2秒押し続けると削除します(キーボードは Space か Enter を押し続けます)</p>
<p class="htc-status" id="htc-status" role="status" aria-live="polite"></p>
</div>
.htc-stage { display:flex; flex-direction:column; align-items:center; justify-content:center; gap:.8rem; padding:2rem 1.2rem; background:#0B0B10; min-height:280px; text-align:center; }
.htc-btn { position:relative; width:220px; height:52px; border:1px solid rgba(248,113,113,.55); border-radius:12px; overflow:hidden; cursor:pointer;
background:#1C1214; color:#FCA5A5; font-weight:600; font-size:.92rem; touch-action:none; user-select:none; -webkit-user-select:none; -webkit-touch-callout:none; }
.htc-btn:focus-visible { outline:2px solid #fff; outline-offset:3px; }
.htc-fill { position:absolute; inset:0; background:#DC2626; transform-origin:left center; transform:scaleX(0); }
.htc-btn:not(.is-holding) .htc-fill { transition: transform .25s ease, background .3s ease; }
.htc-label { position:relative; }
.htc-btn.is-holding .htc-label, .htc-btn.is-done .htc-label { color:#fff; }
.htc-btn.is-done { border-color:#34D399; }
.htc-btn.is-done .htc-fill { background:#047857; transform:scaleX(1); }
.htc-help { margin:0; max-width:300px; color:#A1A1AA; font-size:.74rem; line-height:1.6; }
.htc-status { margin:0; min-height:1.2em; color:#C9C9D1; font-size:.8rem; }
@media (prefers-reduced-motion: reduce) {
.htc-btn:not(.is-holding) .htc-fill { transition-duration:.01ms; }
}
(function(){
var btn = document.getElementById('htc-btn');
var fill = document.getElementById('htc-fill');
var label = document.getElementById('htc-label');
var status = document.getElementById('htc-status');
if (!btn || !fill || !label || !status) return;
var HOLD = 1200, start = 0, raf = 0, done = false, armed = false, armTimer = 0;
function complete(){
done = true; armed = false; clearTimeout(armTimer);
btn.classList.remove('is-holding'); btn.classList.add('is-done');
fill.style.transform = '';
label.textContent = '✓ 削除しました';
status.textContent = '削除しました';
setTimeout(reset, 2200);
}
function frame(now){
var p = Math.min((now - start) / HOLD, 1);
fill.style.transform = 'scaleX(' + p + ')';
if (p < 1) { raf = requestAnimationFrame(frame); return; }
raf = 0; complete();
}
function begin(){
if (raf || done) return;
start = performance.now();
btn.classList.add('is-holding');
status.textContent = '押し続けてください…';
raf = requestAnimationFrame(frame);
}
function cancel(){
if (!raf) return;
cancelAnimationFrame(raf); raf = 0;
btn.classList.remove('is-holding');
fill.style.transform = 'scaleX(0)';
status.textContent = '途中で離したので削除していません';
}
function reset(){
done = false; armed = false; clearTimeout(armTimer);
btn.classList.remove('is-done');
fill.style.transform = 'scaleX(0)';
label.textContent = '長押しで削除';
status.textContent = '';
}
btn.addEventListener('pointerdown', function(e){ if (e.button !== 0) return; btn.setPointerCapture(e.pointerId); begin(); });
btn.addEventListener('pointerup', cancel);
btn.addEventListener('pointercancel', cancel);
btn.addEventListener('keydown', function(e){ if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); if (!e.repeat) begin(); } });
btn.addEventListener('keyup', function(e){ if (e.key === ' ' || e.key === 'Enter') { e.preventDefault(); cancel(); } });
btn.addEventListener('blur', cancel);
btn.addEventListener('contextmenu', function(e){ e.preventDefault(); });
btn.addEventListener('click', function(e){
if (e.detail !== 0 || raf || done) return;
if (!armed) {
armed = true;
label.textContent = 'もう一度押すと削除';
status.textContent = '確認のため、もう一度押すと削除します';
armTimer = setTimeout(function(){ if (!done) { armed = false; label.textContent = '長押しで削除'; status.textContent = ''; } }, 5000);
return;
}
complete();
});
})();
AI PROMPT
押し続けるとボタンの中が満ちていき、満ちきった時だけ実行される長押しの確認ボタン(Hold to Confirm)を作って。ボタンは幅と高さを固定し、overflow:hidden の中に絶対配置の塗り(赤)を置いて transform: scaleX(0〜1)(transform-origin: left center)で伸ばす。pointerdown で setPointerCapture を呼び、requestAnimationFrame で押し始めからの経過時間 ÷ 1.2秒 を進み具合にして塗りを伸ばす。1.2秒に達したら実行(塗りを緑に変え、ラベルを「✓ 削除しました」にし、2.2秒後に元へ戻す)。途中で pointerup・pointercancel・フォーカスが外れた時は取り消して、塗りを .25秒で0へ戻し「途中で離したので削除していません」と伝える。キーボードでは Space か Enter の keydown で開始し、keyup で取り消す(押し続けて繰り返し届く keydown=e.repeat でも preventDefault は必ず呼び、標準の click が繰り返し起きないようにする)。長押しでスマホのメニューや文字選択が出ないよう、touch-action:none・user-select:none・-webkit-touch-callout:none を付け、contextmenu を止める。長押しは誰にでもできる操作ではないため、代わりの手段として、押した回数(event.detail)が0の click には1回目で「もう一度押すと削除」に変え、5秒以内の2回目で実行する(マウスや指で押した click は detail がクリック回数=1以上になるので対象外)。操作方法(「1.2秒押し続けると削除します」)はボタンの下に文章で示し、aria-describedby で関連付ける。状態は role="status" aria-live="polite" の段落で伝える。@media (prefers-reduced-motion: reduce) では戻る時の transition を無効化する(押している間の塗りは進み具合を示す情報なので残す)。 Acceptance criteria: works with prefers-reduced-motion (animation disabled), zero console errors, zero layout shift
AIエージェントでの実装ガイド → Claude Code でWebアニメーションを実装する
BOOKS · PR · AMAZON
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
ボタンを一定の時間押し続けた時だけ操作を実行し、途中で離せば取り消しになるボタンです。押している間はボタンの中が満ちていくので、あとどれくらい押せば実行されるかが目で分かります。削除や退会のように、うっかり実行されると困る操作の誤操作防止に使われます。
当サイトの Download Progress Button は押したあとに処理の進み具合を見せるボタン、Button 3D Press は押し込む手応えを見せるボタン、Toast Notification は操作の結果を一時的に知らせる通知です。本ページは押している時間そのものを確認の手段にする別テーマです。
長押しは便利な反面、知らなければ使えない操作です。操作方法をボタンと説明文の両方で示し、キーボードでも同じ操作ができるようにしてください。取り返しのつかない操作では、実行後に元に戻せる手段も用意しておくと安心です。
長押しの確認はどんな操作に向いていますか?
削除や送信の取り消しのように、誤って押すと困るけれど頻繁に行う操作に向いています。確認のダイアログを毎回出すより手早く、うっかり触れただけでは実行されません。ただし長押しは見ただけでは気づきにくい操作なので、「長押しで削除」のようにボタン自体に操作方法を書き、説明文も添えてください。
キーボードや支援技術ではどう操作しますか?
キーを押し続けると keydown が繰り返し発生するため、最初の1回(e.repeat が false の時)だけで開始し、keyup で取り消します。ボタンの標準の動作(Space や Enter でのクリック)とぶつからないよう、繰り返しの keydown も含めて preventDefault を呼んでいます。長押し自体が難しい利用者もいるため、押した回数(event.detail)が0の click には「2回続けて押すと実行」という代わりの手段を用意しています。MDN によると click の detail はクリック回数なので、マウスや指で押した click はこの代わりの手段の対象になりません。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。