ドラッグ並べ替え

HTML5 Drag and Dropでリスト項目の順序を入れ替え。挿入位置のヒント線付きで、タスクの優先順位付けや並び替えUIに使えます。

#javascript#drag-and-drop#css

ライブデモ

使用例(お題: アイドルグループ Sakura)

この技法を「アイドルグループ Sakura」というテーマのダミーサイトで実際に使った例です。

HTML
<!-- Sakura:推し楽曲ランキングをドラッグで並べ替え -->
<div class="idol">
  <header class="idol__bar">
    <span class="idol__logo">🌸 Sakura</span>
    <span class="idol__sub">あなたの推し曲ランキング</span>
  </header>

  <p class="idol__lead">ドラッグして好きな順に並べ替えてください</p>

  <ul class="dnd" id="list">
    <li class="dnd__item" draggable="true">
      <span class="dnd__grip" aria-hidden="true">⠿</span>
      <span class="dnd__thumb" style="background-image:url('https://picsum.photos/80/80?random=31')"></span>
      <span class="dnd__body"><span class="dnd__title">春風センセーション</span><span class="dnd__meta">3rd Single</span></span>
    </li>
    <li class="dnd__item" draggable="true">
      <span class="dnd__grip" aria-hidden="true">⠿</span>
      <span class="dnd__thumb" style="background-image:url('https://picsum.photos/80/80?random=32')"></span>
      <span class="dnd__body"><span class="dnd__title">夜空のラムネ</span><span class="dnd__meta">2nd Single</span></span>
    </li>
    <li class="dnd__item" draggable="true">
      <span class="dnd__grip" aria-hidden="true">⠿</span>
      <span class="dnd__thumb" style="background-image:url('https://picsum.photos/80/80?random=33')"></span>
      <span class="dnd__body"><span class="dnd__title">きみとパレード</span><span class="dnd__meta">5th Single</span></span>
    </li>
    <li class="dnd__item" draggable="true">
      <span class="dnd__grip" aria-hidden="true">⠿</span>
      <span class="dnd__thumb" style="background-image:url('https://picsum.photos/80/80?random=34')"></span>
      <span class="dnd__body"><span class="dnd__title">花びらメモリー</span><span class="dnd__meta">1st Single</span></span>
    </li>
  </ul>
</div>
CSS
/* Sakura アイドル テーマ */
:root{--pink:#ffd1e0;--deep:#e86a96;--ink:#4a3540;--line:#f0dde4;--muted:#9b8690;--gray:#f7f3f5}
*{box-sizing:border-box}
body{margin:0;min-height:100vh;font-family:"Hiragino Kaku Gothic ProN","Segoe UI",sans-serif;background:linear-gradient(180deg,#fff5f9,var(--gray));color:var(--ink)}
.idol{max-width:440px;margin:0 auto;padding:0 16px 20px}
.idol__bar{display:flex;align-items:center;justify-content:space-between;padding:16px 4px}
.idol__logo{font-weight:800;letter-spacing:.04em;color:var(--deep)}
.idol__sub{font-size:.76rem;color:var(--muted)}
.idol__lead{margin:0 0 14px;font-size:.82rem;color:var(--muted)}
.dnd{list-style:none;margin:0;padding:0;counter-reset:rank}
.dnd__item{
  position:relative;display:flex;align-items:center;gap:12px;
  background:#fff;border:1px solid var(--line);border-radius:14px;
  padding:10px 14px 10px 40px;margin-bottom:10px;cursor:grab;
  box-shadow:0 4px 14px rgba(232,106,150,.08);transition:box-shadow .2s,transform .12s;
}
.dnd__item::before{
  counter-increment:rank;content:counter(rank);
  position:absolute;left:12px;top:50%;transform:translateY(-50%);
  width:20px;height:20px;border-radius:50%;background:var(--pink);color:var(--deep);
  font-size:.72rem;font-weight:800;display:grid;place-items:center;
}
.dnd__item:active{cursor:grabbing}
.dnd__item.dragging{opacity:.5;box-shadow:0 10px 24px rgba(232,106,150,.2)}
.dnd__grip{color:var(--pink);font-size:1rem;flex:none}
.dnd__thumb{width:42px;height:42px;border-radius:10px;background-size:cover;background-position:center;flex:none}
.dnd__body{display:flex;flex-direction:column;gap:2px}
.dnd__title{font-weight:700;font-size:.92rem}
.dnd__meta{font-size:.72rem;color:var(--muted)}
/* ドロップ位置のヒント線 */
.dnd__item.drop-before{box-shadow:0 -3px 0 var(--deep)}
.dnd__item.drop-after{box-shadow:0 3px 0 var(--deep)}
@media (prefers-reduced-motion:reduce){.dnd__item{transition:none}}
JavaScript
// HTML5 Drag and Drop で推し曲ランキングを並べ替え
const list = document.getElementById('list');
let dragging = null;

if (list) {
  // ドロップ位置ヒントをクリア
  const clearHints = () => {
    list.querySelectorAll('.drop-before,.drop-after')
      .forEach((el) => el.classList.remove('drop-before', 'drop-after'));
  };

  list.addEventListener('dragstart', (e) => {
    const item = e.target.closest('.dnd__item');
    if (!item) return;
    dragging = item;
    requestAnimationFrame(() => item.classList.add('dragging')); // 次フレームで半透明化
    e.dataTransfer.effectAllowed = 'move';
  });

  list.addEventListener('dragend', () => {
    if (dragging) dragging.classList.remove('dragging');
    clearHints();
    dragging = null;
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault(); // ドロップを許可
    const over = e.target.closest('.dnd__item');
    if (!over || over === dragging) return;
    clearHints();
    // 上半分/下半分で挿入位置を決定
    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.classList.add(after ? 'drop-after' : 'drop-before');
  });

  list.addEventListener('drop', (e) => {
    e.preventDefault();
    const over = e.target.closest('.dnd__item');
    if (!over || !dragging || over === dragging) return;
    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.insertAdjacentElement(after ? 'afterend' : 'beforebegin', dragging);
    clearHints();
  });
}

コード

HTML
<!-- ドラッグ並べ替え:HTML5 Drag&Drop でリストの順序を入れ替え -->
<div class="board">
  <h2 class="board__title">やることリスト</h2>
  <p class="board__hint">行をドラッグして優先順位を入れ替えられます。</p>

  <ul class="dnd" id="list">
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>市場リサーチをまとめる</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>ワイヤーフレーム作成</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>デザインレビュー依頼</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>実装タスクへ分解</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>リリースノートを書く</li>
  </ul>
</div>
CSS
:root{
  --bg:#101826;
  --card:#18233a;
  --item:#1f2c47;
  --accent:#34d399;
  --text:#dbe4f3;
  --muted:#8a97b3;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#1d3a4a,transparent),var(--bg);
}
.board{width:min(440px,100%)}
.board__title{margin:0 0 4px;font-size:1.15rem;display:flex;align-items:center;gap:10px}
.board__title::before{
  content:"";width:8px;height:20px;border-radius:4px;
  background:linear-gradient(var(--accent),#3b82f6);
}
.board__hint{margin:0 0 16px;color:var(--muted);font-size:.85rem}
.dnd{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
.dnd__item{
  display:flex;align-items:center;gap:12px;
  padding:14px 16px;
  background:var(--item);
  border:1px solid #2a3a5c;border-radius:12px;
  cursor:grab;user-select:none;
  font-size:.94rem;
  transition:transform .18s,box-shadow .25s,background .2s,opacity .2s;
}
.dnd__item:hover{background:#243454}
/* つかんでいる要素 */
.dnd__item.dragging{
  opacity:.4;cursor:grabbing;
  box-shadow:0 14px 30px -12px rgba(0,0,0,.6);
}
/* ドロップ先のヒント線 */
.dnd__item.drop-before{box-shadow:inset 0 3px 0 -1px var(--accent)}
.dnd__item.drop-after{box-shadow:inset 0 -3px 0 -1px var(--accent)}
.dnd__grip{color:var(--muted);letter-spacing:-2px;font-size:1.1rem;line-height:1}
@media (prefers-reduced-motion:reduce){.dnd__item{transition:none}}
JavaScript
// HTML5 Drag and Drop で並べ替え
const list = document.getElementById('list');
let dragging = null;

if (list) {
  // ドロップ位置ヒントをクリア
  const clearHints = () => {
    list.querySelectorAll('.drop-before,.drop-after')
      .forEach((el) => el.classList.remove('drop-before', 'drop-after'));
  };

  list.addEventListener('dragstart', (e) => {
    const item = e.target.closest('.dnd__item');
    if (!item) return;
    dragging = item;
    // 次フレームで半透明化(ゴースト画像はそのまま)
    requestAnimationFrame(() => item.classList.add('dragging'));
    e.dataTransfer.effectAllowed = 'move';
  });

  list.addEventListener('dragend', () => {
    if (dragging) dragging.classList.remove('dragging');
    clearHints();
    dragging = null;
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault(); // ドロップを許可
    const over = e.target.closest('.dnd__item');
    if (!over || over === dragging) return;
    clearHints();

    // マウス位置が要素の上半分か下半分かで挿入位置を決定
    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.classList.add(after ? 'drop-after' : 'drop-before');
  });

  list.addEventListener('drop', (e) => {
    e.preventDefault();
    const over = e.target.closest('.dnd__item');
    if (!over || !dragging || over === dragging) return;

    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.insertAdjacentElement(after ? 'afterend' : 'beforebegin', dragging);
    clearHints();
  });
}

🤖 AIエージェント用プロンプト

このままコピーしてAIに貼り付け「追加する場所」だけ書き換えればOK
あなたは熟練のフロントエンドエンジニアです。私のWebサイトに「ドラッグ並べ替え」の効果を追加してください。

# 追加してほしい効果
ドラッグ並べ替え(UIコンポーネント)
HTML5 Drag and Dropでリスト項目の順序を入れ替え。挿入位置のヒント線付きで、タスクの優先順位付けや並び替えUIに使えます。

# 追加する場所
👉【ここに対象箇所を記入:例「トップのヒーローセクション」「お問い合わせボタン」「記事カードの一覧」など】

# 参考実装(この見た目・挙動を再現してください)
【HTML】
<!-- ドラッグ並べ替え:HTML5 Drag&Drop でリストの順序を入れ替え -->
<div class="board">
  <h2 class="board__title">やることリスト</h2>
  <p class="board__hint">行をドラッグして優先順位を入れ替えられます。</p>

  <ul class="dnd" id="list">
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>市場リサーチをまとめる</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>ワイヤーフレーム作成</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>デザインレビュー依頼</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>実装タスクへ分解</li>
    <li class="dnd__item" draggable="true"><span class="dnd__grip" aria-hidden="true">⋮⋮</span>リリースノートを書く</li>
  </ul>
</div>

【CSS】
:root{
  --bg:#101826;
  --card:#18233a;
  --item:#1f2c47;
  --accent:#34d399;
  --text:#dbe4f3;
  --muted:#8a97b3;
}
*{box-sizing:border-box}
body{
  margin:0;min-height:100vh;
  display:grid;place-items:center;padding:26px 16px;
  font-family:"Segoe UI",system-ui,sans-serif;color:var(--text);
  background:radial-gradient(700px 360px at 50% -10%,#1d3a4a,transparent),var(--bg);
}
.board{width:min(440px,100%)}
.board__title{margin:0 0 4px;font-size:1.15rem;display:flex;align-items:center;gap:10px}
.board__title::before{
  content:"";width:8px;height:20px;border-radius:4px;
  background:linear-gradient(var(--accent),#3b82f6);
}
.board__hint{margin:0 0 16px;color:var(--muted);font-size:.85rem}
.dnd{list-style:none;margin:0;padding:0;display:flex;flex-direction:column;gap:9px}
.dnd__item{
  display:flex;align-items:center;gap:12px;
  padding:14px 16px;
  background:var(--item);
  border:1px solid #2a3a5c;border-radius:12px;
  cursor:grab;user-select:none;
  font-size:.94rem;
  transition:transform .18s,box-shadow .25s,background .2s,opacity .2s;
}
.dnd__item:hover{background:#243454}
/* つかんでいる要素 */
.dnd__item.dragging{
  opacity:.4;cursor:grabbing;
  box-shadow:0 14px 30px -12px rgba(0,0,0,.6);
}
/* ドロップ先のヒント線 */
.dnd__item.drop-before{box-shadow:inset 0 3px 0 -1px var(--accent)}
.dnd__item.drop-after{box-shadow:inset 0 -3px 0 -1px var(--accent)}
.dnd__grip{color:var(--muted);letter-spacing:-2px;font-size:1.1rem;line-height:1}
@media (prefers-reduced-motion:reduce){.dnd__item{transition:none}}

【JavaScript】
// HTML5 Drag and Drop で並べ替え
const list = document.getElementById('list');
let dragging = null;

if (list) {
  // ドロップ位置ヒントをクリア
  const clearHints = () => {
    list.querySelectorAll('.drop-before,.drop-after')
      .forEach((el) => el.classList.remove('drop-before', 'drop-after'));
  };

  list.addEventListener('dragstart', (e) => {
    const item = e.target.closest('.dnd__item');
    if (!item) return;
    dragging = item;
    // 次フレームで半透明化(ゴースト画像はそのまま)
    requestAnimationFrame(() => item.classList.add('dragging'));
    e.dataTransfer.effectAllowed = 'move';
  });

  list.addEventListener('dragend', () => {
    if (dragging) dragging.classList.remove('dragging');
    clearHints();
    dragging = null;
  });

  list.addEventListener('dragover', (e) => {
    e.preventDefault(); // ドロップを許可
    const over = e.target.closest('.dnd__item');
    if (!over || over === dragging) return;
    clearHints();

    // マウス位置が要素の上半分か下半分かで挿入位置を決定
    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.classList.add(after ? 'drop-after' : 'drop-before');
  });

  list.addEventListener('drop', (e) => {
    e.preventDefault();
    const over = e.target.closest('.dnd__item');
    if (!over || !dragging || over === dragging) return;

    const rect = over.getBoundingClientRect();
    const after = e.clientY > rect.top + rect.height / 2;
    over.insertAdjacentElement(after ? 'afterend' : 'beforebegin', dragging);
    clearHints();
  });
}

# 外部ライブラリ
なし(追加ライブラリ不要)

# 守ってほしいこと
- 既存のHTML構造・レイアウト・デザインを壊さないこと。必要に応じてクラス名・色・サイズを私のサイトに合わせて調整してよい。
- クラス名やidが既存と衝突しないよう、必要なら接頭辞で名前空間を分けること。
- レスポンシブ対応と prefers-reduced-motion への配慮を入れること。
- 私のサイトのフレームワーク(React / Vue / 素のHTML など)に合わせて実装すること。不明な場合は素のHTML/CSS/JSで提示し、組み込み手順も説明すること。
- 変更後の確認手順も簡潔に教えてください。