No.172
BUTTON CSSJS

Hold to Confirm Button

押し続けるとボタンの中が満ちていき、満ちきった時だけ実行される長押しの確認ボタン。途中で離せば取り消しになり、キーボードでも Space か Enter を押し続けて操作できます。

DEMO

ボタンを1.2秒押し続けると削除します(キーボードは Space か Enter を押し続けます)

CODE

HTML
<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>
CSS
.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; }
}
JS
(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

Claude Code Cursor v0
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

📖 なかしまぁ先生のCSSアニメーション入門 📖 UIデザインの心理学 ―わかりやすさ・使いやすさの法則 📖 Fundamentals of Web Animation with GSAP

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

学びを止めない · AMAZONサブスク

📚 Kindle Unlimited — 技術書・デザイン書が読み放題。30日無料体験 → 🎧 Audible — 移動中に耳で学ぶオーディオブック。30日無料体験 → 📦 Amazon Prime — Prime Reading・配送特典つき。30日無料体験 → 🎵 Music Unlimited — 作業用BGMに。1億曲が聴き放題・30日無料体験 →

※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。

RELATED EFFECTS

↗ MORE
→ DOWNLOAD PROGRESS BUTTON → BUTTON 3D PRESS → TOAST NOTIFICATION

Hold to Confirm(長押しで実行するボタン)とは

ボタンを一定の時間押し続けた時だけ操作を実行し、途中で離せば取り消しになるボタンです。押している間はボタンの中が満ちていくので、あとどれくらい押せば実行されるかが目で分かります。削除や退会のように、うっかり実行されると困る操作の誤操作防止に使われます。

当サイトの 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プロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。