本文へスキップ

スナックバー+取り消し

リスト項目を削除すると「削除しました [取り消す]」が下部に出て3秒で自動消滅。取り消しで元に戻る。

プレビュー

クリック

操作クリック/タップで動きます。

実装メモ

プレビューは枠内に収めるため、スナックバーを position:absolute(ステージ基準)で実装しています。実際のページで画面下端に固定するときは position:fixed; bottom に変え、body 直下など高い階層へ置いてください。

使いどころ

「取り消し可能な破壊的操作」に向くパターンで、削除・アーカイブ・既読化のように、即座に反映しても数秒以内なら覆せる操作が対象になる。確認ダイアログ(本当に削除しますかと操作の前に止める)とは逆に、こちらは操作を先に実行してから「やり直せますよ」と後から伝える設計で、毎回の削除にダイアログを挟むより体感の速度が上がる。向かないのは、取り消しても実害が残る操作(送信済みメールの取り消しは相手に届いた後では意味をなさない場合がある)や、複数件を続けて操作するバルク処理(このサンプルの実装は保留中の削除を1件しか保持できず、後述のとおり連続削除では前の分が取り消せなくなる)。またログアウトのような影響範囲の大きい操作は、スナックバーの数秒間だけでは取り消しの判断時間として短すぎるため確認ダイアログの方が適する。

仕組み

削除ボタンを押した瞬間は実際にはDOMから消さず、対象のliにremovingクラス(opacity:0; transform:translateX(12px); pointer-events:none)を付けて視覚的に隠すだけに留める。スナックバーが3秒後に自動でcommitRemoveを実行して初めてremovedItem.remove()が呼ばれ、本当にDOMから取り除かれる。取り消しボタンはremovingクラスを外すだけで、要素自体は最初から存在し続けているため、取り消し処理は単なるクラスの付け外しで済み、サーバーへの削除リクエストを送るタイミングもこのcommitRemoveの位置に合わせる必要がある(クリック直後に削除APIを叩いてしまうと、後から取り消しようがなくなる)。保留できる項目はremovedItemという単一の変数で管理されており、複数件を同時に保留する仕組みは持たない。

実装の注意

最大の罠は保留枠が1件しかないことで、コード中の「既に削除待ちがあれば即コミット」という分岐がそれを示している。項目Aを削除した直後(3秒以内)に項目Bも削除すると、Aの取り消し猶予は失われて即座に確定してしまう。連続削除やバルク削除を扱うなら、保留を配列にしてスナックバーを「2件を削除しました」とまとめる設計に変える必要がある。次に、削除の実処理(DOM除去やAPI呼び出し)をcommitRemoveのタイミングに正しく揃えないと、見た目は消えているのにサーバー上はまだ削除されていない不整合が起きる。実装上は「取り消し可能な間はローカルの見た目だけ変える、確定してから初めて実際の削除処理を呼ぶ」という順序を厳守する必要がある。また自動消滅までの3秒は固定値で、ホバーやフォーカスで一時停止する仕組みが無いため、読んでいる途中や取り消そうとしている最中に消えることがある。

アクセシビリティ

スナックバー要素にはrole=status aria-live=polite aria-atomic=trueが最初から付いており、出現時に「削除しました」という中身がひとまとまりで読み上げられる土台はできている。一方で、取り消しボタンへのフォーカス移動は実装されていない。削除ボタンをクリックした後もフォーカスは元の削除ボタンに残ったままで、キーボードやスクリーンリーダーの利用者が取り消すを押すには、3秒の自動消滅までの間に自力でTabして辿り着く必要がある。トーストと違って取り消すという明確な操作を求めるUIなので、フォーカスを奪うことへの配慮とのバランスは要るが、少なくともhoverやfocusが要素内にある間はタイマーを止める対応(現状は未実装)は入れるべきである。これをしないと、読み上げが終わる前や取り消しボタンに辿り着く前に自動で消えてしまい、実質的に取り消し不能になる利用者が出る。

調整して使う

表示時間は3秒が既定だが、破壊的度合いに応じて調整するとよい。単なる既読化のような軽い操作は2秒程度でもよく、完全削除に近い操作は5〜7秒に伸ばして取り消しの猶予を厚くする。複数件をまとめる設計にする場合は「3件を削除しました」とカウントを出し、取り消すと全件が戻る形にする。取り消しボタンの代わりに詳細を見る・共有するといった別のセカンダリアクションを載せるバリエーションもあるが、スナックバーは元来「直前の操作を覆す」ためのUIなので、無関係なアクションを増やすと役割が曖昧になり避けた方がよい。位置は下部中央が定番だが、モバイルではタブバーやセーフエリアと重なるためenv(safe-area-inset-bottom)を加味した位置に調整し、デスクトップでは操作した一覧に近い左下・右下に寄せると視線移動が少ない。

コード(コピーして使えます)

<div class="stage" id="stage">
  <ul class="item-list" id="itemList">
    <li class="item" data-id="1">
      <span class="item-text">タスク A</span>
      <button class="del-btn" aria-label="削除">
        <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
          <line x1="2" y1="2" x2="11" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
          <line x1="11" y1="2" x2="2" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
        </svg>
      </button>
    </li>
    <li class="item" data-id="2">
      <span class="item-text">タスク B</span>
      <button class="del-btn" aria-label="削除">
        <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
          <line x1="2" y1="2" x2="11" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
          <line x1="11" y1="2" x2="2" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
        </svg>
      </button>
    </li>
    <li class="item" data-id="3">
      <span class="item-text">タスク C</span>
      <button class="del-btn" aria-label="削除">
        <svg width="13" height="13" viewBox="0 0 13 13" fill="none">
          <line x1="2" y1="2" x2="11" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
          <line x1="11" y1="2" x2="2" y2="11" stroke="currentColor" stroke-width="2" stroke-linecap="round"/>
        </svg>
      </button>
    </li>
  </ul>

  <div class="snackbar" id="snackbar" role="status" aria-live="polite" aria-atomic="true">
    <span class="snackbar-msg" id="snackbarMsg">削除しました</span>
    <button class="undo-btn" id="undoBtn">取り消す</button>
  </div>
</div>

タグ

同じカテゴリ「通知・オーバーレイ」のほかのパーツ

関連パーツ