DEMO
マウスでつかんで左右に動かせます(タッチは指で、キーボードは矢印キーで)
CODE
<div class="demo-stage drg-stage">
<div class="drg-box">
<div class="drg-row" id="drg-row" tabindex="0" role="region" aria-label="作品一覧(横にスクロールできます)">
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 01</span><span class="drg-sub">モーション</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 02</span><span class="drg-sub">タイポグラフィ</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 03</span><span class="drg-sub">イラスト</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 04</span><span class="drg-sub">写真</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 05</span><span class="drg-sub">UI デザイン</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 06</span><span class="drg-sub">ロゴ</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 07</span><span class="drg-sub">パッケージ</span></button>
<button type="button" class="drg-card"><span class="drg-thumb" aria-hidden="true"></span><span class="drg-title">作品 08</span><span class="drg-sub">映像</span></button>
</div>
<p class="drg-status" id="drg-status" role="status" aria-live="polite">マウスでつかんで左右に動かせます(タッチは指で、キーボードは矢印キーで)</p>
</div>
</div>
.drg-stage { display:flex; align-items:center; justify-content:center; padding:1.5rem 0; background:#0B0B10; min-height:300px; }
.drg-box { width:100%; max-width:560px; min-width:0; }
.drg-row { display:flex; gap:.8rem; padding:.4rem 1.2rem 1rem; overflow-x:auto; scroll-snap-type:x mandatory; scroll-padding:0 1.2rem; cursor:grab; outline:none; }
.drg-row:focus-visible { outline:2px solid #E91B89; outline-offset:-2px; border-radius:12px; }
.drg-row.is-dragging { cursor:grabbing; scroll-snap-type:none; user-select:none; -webkit-user-select:none; }
.drg-card { flex:0 0 160px; scroll-snap-align:start; display:flex; flex-direction:column; gap:.3rem; padding:.6rem; border-radius:14px; border:1px solid rgba(255,255,255,.1); background:#15151B; color:#EDEDED; font:inherit; text-align:left; cursor:inherit; }
.drg-card:focus-visible { outline:2px solid #E91B89; outline-offset:2px; }
.drg-thumb { display:block; height:100px; margin-bottom:.3rem; border-radius:10px; background:linear-gradient(135deg,#7B37A8,#E91B89); }
.drg-card:nth-child(4n+2) .drg-thumb { background:linear-gradient(135deg,#0369A1,#5BAADE); }
.drg-card:nth-child(4n+3) .drg-thumb { background:linear-gradient(135deg,#92400E,#F59E0B); }
.drg-card:nth-child(4n+4) .drg-thumb { background:linear-gradient(135deg,#047857,#34D399); }
.drg-title { font-size:.86rem; font-weight:600; }
.drg-sub { color:#A1A1AA; font-size:.72rem; }
.drg-status { margin:.2rem 1.2rem 0; min-height:1.2em; color:#A1A1AA; font-size:.76rem; line-height:1.6; }
(function(){
var row = document.getElementById('drg-row');
var status = document.getElementById('drg-status');
if (!row || !status) return;
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)');
var down = false, dragging = false, moved = false, startX = 0, startLeft = 0, lastX = 0, lastT = 0, v = 0, raf = 0;
function stopGlide(){ cancelAnimationFrame(raf); raf = 0; }
row.addEventListener('pointerdown', function(e){
if (e.pointerType !== 'mouse' || e.button !== 0) return;
if (e.target === row && e.offsetY >= row.clientHeight) return;
stopGlide();
row.classList.remove('is-dragging');
down = true; dragging = false; moved = false;
startX = lastX = e.clientX; startLeft = row.scrollLeft; lastT = e.timeStamp; v = 0;
});
row.addEventListener('pointermove', function(e){
if (!down) return;
if (!(e.buttons & 1)) { down = false; dragging = false; row.classList.remove('is-dragging'); return; }
var dx = e.clientX - startX;
if (!dragging) {
if (Math.abs(dx) < 6) return;
dragging = moved = true;
row.setPointerCapture(e.pointerId);
row.classList.add('is-dragging');
}
row.scrollLeft = startLeft - dx;
var dt = e.timeStamp - lastT;
if (dt > 0) v = (lastX - e.clientX) / dt * 16;
lastX = e.clientX; lastT = e.timeStamp;
});
function release(e){
if (!down) return;
down = false;
if (!dragging) return;
dragging = false;
if (e.timeStamp - lastT > 100) v = 0;
if (reduce.matches || Math.abs(v) < 1) { row.classList.remove('is-dragging'); return; }
(function glide(){
row.scrollLeft += v;
v *= 0.92;
var edge = row.scrollLeft <= 0 || row.scrollLeft >= row.scrollWidth - row.clientWidth - 1;
if (Math.abs(v) > 0.5 && !edge) raf = requestAnimationFrame(glide);
else { raf = 0; row.classList.remove('is-dragging'); }
})();
}
row.addEventListener('pointerup', release);
row.addEventListener('pointercancel', function(e){ release(e); moved = false; });
row.addEventListener('dragstart', function(e){ e.preventDefault(); });
row.addEventListener('click', function(e){
if (moved) { e.preventDefault(); e.stopPropagation(); moved = false; return; }
var card = e.target.closest('.drg-card');
if (card) status.textContent = '「' + card.querySelector('.drg-title').textContent + '」を開きました';
}, true);
})();
AI PROMPT
横に並んだカードの列をマウスでつかんで左右に動かし、離すと勢いのまま少し流れてから止まる横スクロール(Drag to Scroll)を作って。列は display:flex・overflow-x:auto・scroll-snap-type: x mandatory で、カードは幅160pxの button 要素にして scroll-snap-align:start を付ける。列には tabindex="0"・role="region"・aria-label を付けて、キーボードの矢印キーでもスクロールできるようにする。マウスのドラッグは pointerType が mouse の時だけ扱い(タッチはブラウザ標準の横スクロールに任せる。横スクロールバーの上で押した時も対象外)、pointerdown で開始位置と scrollLeft を記録する。6px 以上動いてから初めてドラッグとみなし、その時に setPointerCapture と「ドラッグ中」のクラス(cursor:grabbing・scroll-snap-type:none・user-select:none)を付ける(最初から setPointerCapture すると、普通のクリックがカードに届かなくなるため)。ドラッグ中は scrollLeft = 開始時の scrollLeft − マウスの移動量 にし、直前の移動から1フレームあたりの速度を求める。離したら(離す直前に100ミリ秒以上止まっていた時は流さない)requestAnimationFrame で scrollLeft に速度を足し、速度に0.92を掛けて減らしていき、十分遅くなるか端に着いたら止めて「ドラッグ中」のクラスを外す(scroll-snap が戻ってカードの位置にそろう)。ボタンを押していない pointermove が来たら(列の外で離した時など)ドラッグの状態を解除する。ドラッグした直後の click はキャプチャ段階で止め、少しだけ動かした時はクリックとしてカードを開く。画像のドラッグは dragstart で止める。選んだカードは role="status" の段落で伝える。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
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
学びを止めない · AMAZONサブスク
※当サイトはAmazonアソシエイトプログラムの参加者です。リンクから商品をご購入いただくと、当方に紹介料が発生します。
CREATOR TOOLS · スクール(PR)
※本サイトはアフィリエイトリンクを含みます。リンク経由のご購入・お申し込みで運営者が紹介料を受け取る場合があります。
横に並んだカードや画像の列を、マウスでつかんで左右に動かせるようにする演出です。横スクロールのバーを探して操作しなくても、スマホで指を滑らせるのと同じ感覚でパソコンでも一覧を流せます。離した時に勢いのまま少し流れると、つかんで投げたような手触りになります。
当サイトの Swipe Carousel は scroll-snap だけで作る JavaScript なしのスワイプカルーセル、Horizontal Scroll は縦スクロールの操作で横方向にコンテンツが流れる演出、Infinite Moving Cards はカードが途切れず横に流れ続ける無限カルーセルです。本ページは標準の横スクロールを残したまま、マウスのドラッグと慣性を足す別テーマです。
ドラッグはマウスを使う人のための近道で、キーボードやタッチでも同じ一覧を動かせることが前提です。列にフォーカスできるようにして矢印キーでのスクロールを残し、ドラッグとクリックを距離で区別すると、どちらの操作も邪魔しません。
タッチではドラッグの処理をしないのはなぜですか?
スマホやタブレットのブラウザは、指で横にスワイプするだけで横スクロールでき、指を離した後の慣性の動きも標準で付いています。JavaScript で同じ処理を上書きすると、縦スクロールとの切り分けや慣性の手触りが標準と変わり、かえって使いにくくなります。本実装はマウスで操作する時だけドラッグを扱い、タッチは標準の動きに任せています。
ドラッグした後にカードが開いてしまわないのはなぜですか?
マウスを押して離すと、その間にどれだけ動かしても click イベントが発生します。本実装は6px以上動かした時に「ドラッグした」と記録し、直後の click を列のキャプチャ段階(カードに届く前)で止めています。少しだけ動かした場合はクリックとして扱い、カードを開きます。
コードは無料で使えますか?
はい。Motion LabのコードスニペットとAIプロンプトは、クレジット表記なしで自由に使えます。コピーしてそのままプロジェクトに利用できます。