No.162
CARD CSSJS

Swipe to Dismiss

指やマウスで横にスワイプすると、一定以上で画面の外へ飛んで消え、足りなければ元へ戻る通知カード。スワイプできない人のために「閉じる」ボタンとキー操作も用意しています。

DEMO

    CODE

    HTML
    <div class="demo-stage swd-stage">
      <div class="swd-box">
        <ul class="swd-list" id="swd-list" aria-label="通知"></ul>
        <button type="button" class="swd-reset" id="swd-reset" hidden>通知を元に戻す</button>
      </div>
    </div>
    CSS
    .swd-stage { display:flex; align-items:center; justify-content:center; padding:1.5rem; background:#0B0B10; min-height:300px; overflow:hidden; }
    .swd-box { width:100%; max-width:340px; display:flex; flex-direction:column; gap:.6rem; align-items:center; }
    .swd-list { list-style:none; margin:0; padding:0; width:100%; display:flex; flex-direction:column; gap:.55rem; }
    .swd-card { position:relative; display:flex; align-items:center; gap:.8rem; padding:.8rem .9rem; border-radius:12px; background:#17171C; border:1px solid rgba(255,255,255,.1);
      touch-action:pan-y; user-select:none; cursor:grab; transition: transform .3s cubic-bezier(.22,1,.36,1), opacity .3s ease; }
    .swd-card.is-drag { transition:none; cursor:grabbing; }
    .swd-card:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
    .swd-ic { flex:0 0 34px; height:34px; border-radius:9px; display:flex; align-items:center; justify-content:center; background:rgba(233,27,137,.16); }
    .swd-body { flex:1; min-width:0; }
    .swd-t { display:block; color:#EDEDED; font-size:.84rem; }
    .swd-s { display:block; color:#8A8A96; font-size:.72rem; }
    .swd-x { flex:0 0 32px; height:32px; border:0; border-radius:8px; background:transparent; color:#8A8A96; font-size:1rem; cursor:pointer; }
    .swd-x:hover { background:rgba(255,255,255,.08); color:#fff; }
    .swd-x:focus-visible { outline:2px solid #E91B89; }
    .swd-hint { color:#6E6E7A; font-size:.72rem; }
    .swd-reset { min-height:40px; padding:0 1rem; border-radius:999px; border:1px solid rgba(255,255,255,.2); background:transparent; color:#C9C9D1; font-size:.8rem; cursor:pointer; }
    .swd-reset:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
    @media (prefers-reduced-motion: reduce) {
      .swd-card { transition-duration:.01ms; }
    }
    JS
    (function(){
      var list = document.getElementById('swd-list');
      var reset = document.getElementById('swd-reset');
      if (!list || !reset) return;
      var DATA = [['📦', '荷物が発送されました', '2分前'], ['💬', '新しいコメントがあります', '15分前'], ['🔔', '予約の前日になりました', '1時間前']];
      var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
      function remove(card, dir){
        var li = card.parentNode;
        card.style.transform = 'translateX(' + (dir * 120) + '%)'; card.style.opacity = '0';
        setTimeout(function(){
          var sib = li.nextElementSibling || li.previousElementSibling;
          li.remove();
          var target = sib && sib.querySelector('.swd-card');
          if (target) target.focus(); else { reset.hidden = false; reset.focus(); }
        }, reduce.matches ? 0 : 300);
      }
      function build(){
        list.innerHTML = ''; reset.hidden = true;
        DATA.forEach(function(d){
          var li = document.createElement('li');
          var c = document.createElement('div');
          c.className = 'swd-card'; c.tabIndex = 0;
          c.setAttribute('aria-label', d[1] + '(' + d[2] + ')。Delete キーで消去');
          c.innerHTML = '<span class="swd-ic" aria-hidden="true">' + d[0] + '</span><span class="swd-body"><span class="swd-t">' + d[1] + '</span><span class="swd-s">' + d[2] + '</span></span>';
          var x = document.createElement('button');
          x.type = 'button'; x.className = 'swd-x'; x.textContent = '×'; x.setAttribute('aria-label', d[1] + 'を閉じる');
          x.addEventListener('click', function(e){ e.stopPropagation(); remove(c, 1); });
          x.addEventListener('pointerdown', function(e){ e.stopPropagation(); });
          c.appendChild(x);
          var startX = 0, dx = 0, id = null;
          c.addEventListener('pointerdown', function(e){ id = e.pointerId; startX = e.clientX; dx = 0; c.setPointerCapture(id); c.classList.add('is-drag'); });
          c.addEventListener('pointermove', function(e){
            if (e.pointerId !== id) return;
            dx = e.clientX - startX;
            c.style.transform = 'translateX(' + dx + 'px)';
            c.style.opacity = String(1 - Math.min(Math.abs(dx) / c.offsetWidth, 1) * .7);
          });
          function end(e){
            if (e.pointerId !== id) return;
            id = null; c.classList.remove('is-drag');
            if (Math.abs(dx) > c.offsetWidth * .35) remove(c, dx > 0 ? 1 : -1);
            else { c.style.transform = ''; c.style.opacity = ''; }
          }
          c.addEventListener('pointerup', end);
          c.addEventListener('pointercancel', end);
          c.addEventListener('keydown', function(e){ if (e.key === 'Delete' || e.key === 'Backspace') { e.preventDefault(); remove(c, 1); } });
          li.appendChild(c); list.appendChild(li);
        });
      }
      reset.addEventListener('click', function(){ build(); var f = list.querySelector('.swd-card'); if (f) f.focus(); });
      build();
    })();

    AI PROMPT

    Claude Code Cursor v0
    PROMPT
    指やマウスで横にスワイプすると消える通知カード(Swipe to Dismiss)を作って。Pointer Events(pointerdown / pointermove / pointerup / pointercancel)でマウスとタッチをまとめて扱い、pointerdown で setPointerCapture を呼んでカードの外へ指が出ても追跡を続ける。ドラッグ中は transition を切って translateX(移動量) で指に追従させ、移動量に応じて不透明度を下げる。指を離した時、移動量がカード幅の35%を超えていれば、その方向へ translateX(±120%)・透明 にして .3秒で飛ばし、終わったら要素を取り除く。超えていなければ transform を空に戻して元の位置へ戻す(cubic-bezier(.22,1,.36,1))。縦スクロールを妨げないようカードに touch-action: pan-y を付ける。スワイプできない利用者のために、各カードに aria-label 付きの「×」ボタンを置き、カード自体もフォーカスできるようにして Delete キーでも消せるようにする。消したら次のカードへフォーカスを移し、全部消えたら「通知を元に戻す」ボタンを出す。@media (prefers-reduced-motion: reduce) では飛んでいく動きを省略する。
    
    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
    → FRAMER DRAG → TOAST NOTIFICATION → SWIPE CAROUSEL

    Swipe to Dismiss(スワイプで消せるカード)とは

    通知やメールの一覧で、項目を横にスワイプするとその項目が画面の外へ飛んで消える操作です。スマホのOSやメールアプリで広く使われており、片手の親指だけで不要な通知を次々に片づけられます。

    当サイトの Framer Drag は Framer Motion で要素をドラッグする演出、Toast Notification は一時的な通知の表示、Swipe Carousel はスワイプで画像を送るカルーセルです。本ページはスワイプで項目を消す操作と、消すかどうかのしきい値の判定を扱う別テーマです。

    スワイプは便利な反面、使えない人もいる操作です。同じことができるボタンとキー操作を必ず並べ、消したあとのフォーカスの行き先まで決めておくと、誰でも使える一覧になります。誤って消した時のために、元に戻す手段を用意しておくことも大切です。

    よくある質問

    touch-action: pan-y は何のためですか?

    スマホでカードに触れたとき、ブラウザは標準で「スクロールか拡大か」を判断して処理します。touch-action: pan-y を指定すると、縦方向のスクロールはブラウザに任せたまま、横方向の動きだけをページ側で受け取れます。これで縦にスクロールしたいのにカードが横へ動いてしまう、という誤操作を防げます。

    スワイプだけで消せる作りにしてはいけないのですか?

    スワイプは、マウスやキーボードで操作する人、指先の細かい動きが難しい人には使えないことがあります。WCAG 2.2 でも、ドラッグ操作には1回のクリックやタップで代替できる手段を用意することが求められています。本実装は「×」ボタンと Delete キーでも同じ操作ができるようにしています。

    コードは無料で使えますか?

    はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。