【Next.js × DnD-kit】DnD-kitが“浮くのに動かない”問題の解決ガイド(安定化編)
はじめに
看護業務でつかえるかなと、日々学習しながら「看護師向け計算ツールアプリ」を Next.js + TypeScript で開発に挑戦しています。
前回の記事では、DnD-kitでスマホ長押し対応の並び替え機能を実装しました。
しかし実際に動かすと、こうなりました‥‥‥👇
- 長押しするとボタンが“浮く”
- でも 動かない…
- 他のボタンが よけない…
- 順番が 変わらない…
これは DnD-kit で最も多い “つまずきポイント” であり、原因は複数が絡むため、表面的にコードを見ても気付きにくい問題らしく、まんまとはまりました!(笑)
この記事では、
- なぜ「浮くのに動かない」のか
- 原因をどう切り分けるか
- 根本的に安定させる“最終レイアウト”とは?
- スマホでも確実に動くDnDの実装方法
をまとめて解説します。
🧠 結論:DnD-kitが動かない原因は 4つの構造問題
| 原因 | 症状 | 必要な対策 |
|---|---|---|
| ① id 型不一致 | active.id と over.id が一致せず、順番更新できない | id を string に統一 |
| ② flex / grid の自動整列 | “見た目”と“座標”がズレ、順番計算が破綻 | verticalListSortingStrategy + columns |
| ③ transform / ref の適用ミス | 浮くだけで座標移動しない | setNodeRef と transform を正しく適用 |
| ④ grid / flex-wrap で座標取得ズレ | 特にスマホで不安定 | 並び替えモード時だけ縦リスト化 |
最終的な安定構成は👇
✔ 結論:
「縦方向リスト × columnsレイアウト × verticalListSortingStrategy」
が私の実装では最も安定しました。
🩺 ① idがstringで揃っていない(最も多い原因)
DnD-kit は内部で
active.id === over.id
を厳密比較(===)します。
なので、
1 !== "1"
となり、座標は動いていても並び替えが確定できません。
🔧 改善(idを完全にstring化)
const calculators = [
{ id: "medication", name: "投薬計算" },
{ id: "drip", name: "点滴速度" },
];
または初期化時に変換👇
const calculatorsWithStringId = calculators.map(c => ({
...c,
id: String(c.id),
}));
export const DEFAULT_ORDER = calculatorsWithStringId.map(c => c.id);
💥 ② flex / grid だと「座標判定」が破綻する
DnD-kit は「見た目の並び」ではなく
DOMの矩形(x, y, width, height) をもとにソートします。
flex や grid ではブラウザが自動的に折り返しするため、
DnD-kitが読んでいる座標と見た目がズレます。
その結果:
- ボタンが浮くのに動かない
- 他のボタンがよけない
- 順番が変わらない
といった現象が起きます。
🧩 ③ transform / ref を正しく適用していない
DnD-kit の動作は transform: translate3d() を適用することで成立します。
const { attributes, listeners, setNodeRef, transform, transition } =
useSortable({ id });
正しい構造👇
<div
ref={setNodeRef}
style={{
transform: CSS.Transform.toString(transform),
transition,
}}
{...attributes}
{...listeners}
>
<CalcButton id={id} />
</div>
❌ よくあるミス
- ref をボタンに渡してしまう
- transform を自作CSSで上書きする
これをやると「浮くだけで動かない」状態になります。
🧠 ④ grid構造のままだとスマホで特に不安定
(→ columns + verticalListSortingStrategy で解決)
スマホで最も問題になるのが grid / flex-wrap のズレ です。
DnD-kit 公式でも
可変レイアウト(wrap/grid)では座標取得が不安定になる
と明記されています。
🎉 解決:並び替えモード時だけ columnsレイアウト に変更する
ここが今回の最終安定解法でした。
✔ 並び替えモード外→普通の2列UI
✔ 並び替えモード中→縦方向の columns に切り替え
(verticalListSortingStrategy と相性がよい)
🔧 最終レイアウト(スマホ2列/PC3列)
<SortableContext
items={order}
strategy={verticalListSortingStrategy}
>
<ul className="columns-1 sm:columns-2 md:columns-3 space-y-3">
{items.map(item => (
<SortableButton key={item.id} id={item.id} />
))}
</ul>
</SortableContext>
メリット👇
- スマホ:1〜2列でも安定してDnD
- PC:3列でも動く
- 順番が確実に変わる
- 物理シミュレーションが崩れない
🧪 handleDragEnd(最終形)
const handleDragEnd = (event: DragEndEvent) => {
const { active, over } = event;
if (!over || active.id === over.id) return;
const oldIndex = order.indexOf(String(active.id));
const newIndex = order.indexOf(String(over.id));
const newOrder = arrayMove(order, oldIndex, newIndex);
setOrder(newOrder);
saveOrder(newOrder);
};
💾 localStorage(永続化)
// 保存
export const saveOrder = (order: string[]) => {
if (typeof window === "undefined") return;
localStorage.setItem("calcOrder", JSON.stringify(order));
};
// 読み込み
export const loadOrder = () => {
if (typeof window === "undefined") return [];
const data = localStorage.getItem("calcOrder");
return data ? JSON.parse(data) : [];
};
初回読み込み
useEffect(() => {
const saved = loadOrder();
setOrder(saved.length ? saved : DEFAULT_ORDER);
}, []);
📱 optional: delay / tolerance
※次回の記事で詳細に触れるため、ここでは必要最小限だけ。
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: {
delay: 150,
tolerance: 5,
},
})
);
🧠 まとめ:DnD-kitを安定させる“処方箋”
| 問題 | 原因 | 確実な解決策 |
|---|---|---|
| 浮くのに動かない | id不一致 | id を string に統一 |
| 順番かわらない | layoutの自動整列 | columns + verticalListSortingStrategy |
| 他がよけない | transform未適用 | setNodeRef+transformを正しく適用 |
| スマホ不安定 | rect座標ズレ | columns で縦方向に並べる |
🔗 シリーズ一覧
🧩【Next.js × DnD-kit】スマホ対応ドラッグ&ドロップ実装シリーズ(全3回)
1.Next.js × DnD-kitで作るスマホ対応ドラッグ&ドロップ(基礎編)
2.DnD-kitが“浮くのに動かない”問題の解決ガイド(安定化編)←(この記事)
3.スマホUX最適化&並び順を保存する仕組み(実践編)
Discussion