用 Claude Code 实现拖拽:React 与 dnd-kit 实战指南
用 Claude Code 构建可访问的 React 拖拽排序,包含 dnd-kit、TypeScript 代码、常见坑和验证清单。
拖拽功能看起来只是“按住然后移动”,但真正上线时要处理鼠标、触控、键盘、屏幕阅读器、保存时机、误操作取消和移动端布局。
如果只对 Claude Code 说“做一个 drag and drop”,它通常能生成一个视觉上能动的版本,但不一定适合生产环境。更可靠的做法是先说明数据结构、输入方式、键盘规则和验收标准。
本文使用 React + TypeScript + dnd-kit 做一个可运行的任务排序示例,并说明 3 个以上真实用例、常见坑、官方文档链接、站内延伸阅读和商业转化 CTA。
适用场景
| 场景 | 例子 | 审查重点 |
|---|---|---|
| 列表排序 | 待读文章、任务优先级、菜单顺序 | 只在 drop 后保存 |
| 看板 | 销售线索、开发工单、招聘流程 | 支持空列和跨列移动 |
| 文件上传 | 图片、PDF、CSV | 类型和大小验证 |
| 仪表盘编辑 | 图表和组件重排 | 移动端最好单独进入编辑模式 |
基本概念是:用户操作 → sensor 检测输入 → DndContext 发出事件 → 更新 React state → UI 重新渲染。不要把 DOM 顺序当成真实数据,真实顺序应该放在 state 里。
Native API 还是 dnd-kit
MDN 的 HTML Drag and Drop API适合文件拖入浏览器、从外部应用传递数据等场景。React 内部的列表排序、看板和可访问交互,更推荐 dnd-kit。
| 方案 | 适合 | 弱点 |
|---|---|---|
| HTML Drag and Drop API | 文件拖放、外部数据传递 | 触控和复杂排序控制较难 |
| dnd-kit | React 列表、看板、网格 | 需要理解 sensors 和 context |
官方参考:dnd-kit Getting started 和 dnd-kit Accessibility。如果你正在做文件上传,也可以参考站内的文件上传指南。
给 Claude Code 的任务描述
{
"goal": "Build an accessible React drag-and-drop sortable list with TypeScript.",
"stack": ["React", "TypeScript", "@dnd-kit/core", "@dnd-kit/sortable"],
"requirements": [
"Support pointer and keyboard operation",
"Keep React state as the source of truth",
"Use a real button as the drag handle",
"Show visible focus styles",
"Save only after drag end"
],
"verification": [
"Move an item with mouse or trackpad",
"Move an item with keyboard",
"Cancel a drag with Escape",
"Check the layout on mobile width"
]
}
延伸阅读可以看React 开发指南、可访问性实现和测试策略。
安装
npm create vite@latest dnd-demo -- --template react-ts
cd dnd-demo
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
npm run dev
数据模型
创建 src/taskModel.ts。
export type Task = {
id: string;
title: string;
note: string;
};
export const initialTasks: Task[] = [
{ id: "task-1", title: "Write acceptance criteria", note: "Define the done state before coding." },
{ id: "task-2", title: "Build sortable UI", note: "Use dnd-kit sensors and React state." },
{ id: "task-3", title: "Check keyboard flow", note: "Tab, Space, Arrow keys, Enter, Escape." },
{ id: "task-4", title: "Review file fallback", note: "Keep input type=file for upload screens." },
];
React 组件
将 src/App.tsx 替换为下面的代码。
import { useState, type CSSProperties } from "react";
import {
DndContext,
KeyboardSensor,
PointerSensor,
closestCenter,
useSensor,
useSensors,
type DragEndEvent,
} from "@dnd-kit/core";
import {
SortableContext,
arrayMove,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import "./App.css";
import { initialTasks, type Task } from "./taskModel";
export default function App() {
const [tasks, setTasks] = useState<Task[]>(initialTasks);
const sensors = useSensors(
useSensor(PointerSensor, { activationConstraint: { distance: 6 } }),
useSensor(KeyboardSensor, { coordinateGetter: sortableKeyboardCoordinates }),
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
setTasks((currentTasks) => {
const oldIndex = currentTasks.findIndex((task) => task.id === active.id);
const newIndex = currentTasks.findIndex((task) => task.id === over.id);
if (oldIndex < 0 || newIndex < 0) {
return currentTasks;
}
return arrayMove(currentTasks, oldIndex, newIndex);
});
}
return (
<main className="appShell">
<h1>Accessible task ordering</h1>
<p id="dnd-help" className="helpText">
Use Tab to reach a handle. Press Space or Enter to lift, arrow keys to move,
Space or Enter to drop, and Escape to cancel.
</p>
<DndContext sensors={sensors} collisionDetection={closestCenter} onDragEnd={handleDragEnd}>
<SortableContext items={tasks.map((task) => task.id)} strategy={verticalListSortingStrategy}>
<ul className="taskList" aria-describedby="dnd-help">
{tasks.map((task) => (
<SortableTask key={task.id} task={task} />
))}
</ul>
</SortableContext>
</DndContext>
</main>
);
}
function SortableTask({ task }: { task: Task }) {
const {
attributes,
listeners,
setActivatorNodeRef,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id: task.id });
const style: CSSProperties = {
transform: CSS.Transform.toString(transform),
transition,
};
return (
<li ref={setNodeRef} style={style} className={`taskCard ${isDragging ? "dragging" : ""}`}>
<button
ref={setActivatorNodeRef}
type="button"
className="dragHandle"
aria-label={`Move ${task.title}`}
{...attributes}
{...listeners}
>
<span aria-hidden="true">↕</span>
</button>
<div>
<h2>{task.title}</h2>
<p>{task.note}</p>
</div>
</li>
);
}
CSS
创建或替换 src/App.css。
:root {
color: #172033;
background: #f6f7fb;
font-family: Inter, ui-sans-serif, system-ui, sans-serif;
}
body {
margin: 0;
}
button {
font: inherit;
}
.appShell {
max-width: 760px;
margin: 0 auto;
padding: 32px 20px;
}
.helpText {
color: #526070;
line-height: 1.6;
}
.taskList {
display: grid;
gap: 12px;
padding: 0;
list-style: none;
}
.taskCard {
display: grid;
grid-template-columns: 44px minmax(0, 1fr);
gap: 12px;
align-items: start;
padding: 14px;
border: 1px solid #d9deea;
border-radius: 8px;
background: #ffffff;
box-shadow: 0 8px 24px rgba(23, 32, 51, 0.08);
}
.dragging {
opacity: 0.58;
}
.taskCard h2 {
margin: 0 0 4px;
font-size: 1rem;
}
.taskCard p {
margin: 0;
color: #526070;
line-height: 1.5;
}
.dragHandle {
display: grid;
width: 36px;
height: 36px;
place-items: center;
border: 1px solid #c8d0df;
border-radius: 8px;
background: #f8fafc;
cursor: grab;
}
.dragHandle:focus-visible {
outline: 3px solid #2563eb;
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.taskCard {
transition: none;
}
}
可访问性与常见坑
发布前至少检查:Tab 能到达每个拖拽按钮;Space/Enter 可以开始和结束拖拽;方向键可以移动;Escape 可以取消;焦点样式清楚;移动端宽度不遮挡文字。文件上传场景不要只依赖拖拽,保留真实的 <input type="file">。
常见坑有四个:在每次 dragover 时保存到服务器;忘记空列;只支持鼠标;让 Claude Code 自由选择库和交互规则。把验收标准写清楚,代码质量会稳定很多。
商业 CTA 与验证记录
如果团队要把这种交互放进内部产品,建议把规则写进 CLAUDE.md,并参考CLAUDE.md 模板。ClaudeCodeLab 提供Claude Code 培训与咨询以及模板和教材,可以把拖拽、上传、看板等 UI 变成团队可复用的开发标准。
实际验证时,先确定 state 结构再让 Claude Code 编码,返工最少。把键盘移动、Escape 取消、移动端宽度和保存时机加入清单后,能很快发现“鼠标看起来能用但还不能上线”的版本。
总结
用 Claude Code 实现拖拽时,真正决定质量的不是它能多快写出一段会动的代码,而是你有没有在动手前把数据结构、输入方式、键盘规则和保存时机讲清楚。先确定 state 作为唯一可信来源,再让 Claude Code 围绕这个结构生成 sensor、DndContext 和排序逻辑,返工会明显减少。
库的选择也要按场景来。React 内部的列表排序、看板和需要可访问性的交互优先用 dnd-kit,同时启用 PointerSensor 和 KeyboardSensor,并把真正的 <button> 用作拖拽 handle;只有从桌面拖入文件、与外部应用传递数据这类场景,才更适合 MDN 的原生 HTML Drag and Drop API。两者不是二选一的对立,而是各自覆盖不同的需求。
发布前请把这份清单走一遍:用鼠标或触控板能移动条目,用键盘也能移动,Space 或 Enter 能抬起和放下,Escape 能取消,焦点样式清晰可见,移动端宽度下文字不被遮挡。文件上传场景不要只依赖拖拽,务必保留 <input type="file"> 作为键盘可用的后备。把这些验收标准交给 Claude Code 当作 review checklist 的一部分,让它逐条用文件名和操作步骤给出证据,就能在早期发现那种“鼠标看起来能用、却还不能上线”的版本。
几个最常见的坑也值得在收尾时再强调一遍。第一,不要在每次 dragover 时都向服务器保存——拖动过程中事件会反复触发,保存应当只在拖放确定后执行,中间状态交给 React state 即可。第二,别忘了把空列也设为可放置区域,否则一旦某列没有卡片就会卡住,在真实看板里很快就会暴露。第三,永远把 state 当作排序的真实来源,而不是去读取 DOM 顺序;要持久化时,把 state 里的数组发给 API。把这几条连同键盘和可访问性要求一起写进 CLAUDE.md,团队后续生成的拖拽代码质量会稳定得多。
以这个示例为基础,你可以扩展到任务管理、文章排序、图片画廊、销售看板、仪表盘编辑等界面。无论应用到哪一种,先把 state 的形状、保存时机和键盘替代操作定下来,都是最省力、也最稳妥的第一步。
免费 PDF: Claude Code 速查表
输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。
我们会妥善保护你的信息,不发送垃圾邮件。
把 Claude Code 变成真正能带来结果的工作流
先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。
关于作者
Masa
专注 Claude Code 实务流程、团队导入和内容转化的工程师。
相关文章
Claude Code权限安全阶梯:逐步放开访问而不失控
从只读到有限编辑、验证命令和部署检查的 Claude Code 权限升级流程。
Claude Code 小PR证据包:让小改动真正可审查
用差异、验证命令、公开URL、CTA路径和回滚说明,把Claude Code的小PR变得可审查。
Claude Code 提交前 Review Gate:同时检查差异、测试、公开 URL 和 CTA
提交前用 Claude Code 审查差异范围、build、公开 URL、Gumroad 链接、咨询 CTA、缺少测试和无关文件。