用 CSS Grid 实现响应式看板:grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) 实现等宽自适应列,每列 overflow-y: auto、min-height: 60vh,禁用 grid-gap 改用 padding;拖拽需全程 preventDefault(),动态插入 pointer-events: none 占位符辅助定位;iOS 需降级为 touch 事件模拟。
纯 CSS 实现 Kanban 列布局,核心是让每列(column)等宽、自适应、可滚动,同时列内卡片垂直堆叠。用 display: grid 配合 grid-template-columns 最稳妥,避免老式布局带来的高度塌陷、间隙错位问题。
常见错误:用 float: left 拼列,结果在 Safari 下列宽计算异常;或用 inline-block 加 vertical-align,导致列间基线对齐错乱、空格吃宽度。
grid-template-columns: repeat(auto-fit, minmax(280px, 1fr)) —— 自动适配列数,最小列宽 280px,最大均分剩余空间overflow-y: auto,保证长列表可滚动而不撑高整页min-height: 60vh,防止空列塌缩看不见拖拽区grid-gap 改用 padding 控制列间距,避免 gap 在拖拽时干扰 DragEvent 坐标判断draggable="true" 后必须拦截默认行为HTML5 原生拖拽 API 看似简单,但不处理默认行为会导致:拖动瞬间页面文字被选中、图片被拖出浏览器、松手后卡片“消失”(其实是被浏览器当文件下载了)。
关键点在于所有拖拽相关事件都必须调用 event.preventDefault(),否则 drop 事件根本不会触发。
立即学习“前端免费学习笔记(深入)”;
dragstart,设 event.dataTransfer.setData('text/plain', cardId),只传 ID,别传 DOM 节点dragover,必须立刻 event.preventDefault(),否则 drop 不会触发drop,用 event.dataTransfer.getData('text/plain') 拿到 ID,再用 appendChild 或 insertBefore 移动真实 DOM 节点dragenter 里做 heavy work(比如重排卡片),它可能高频触发,造成卡顿drop 事件坐标不可靠原生 drop 事件只告诉你“落在哪个元素上”,但不知道该插在目标列的第几个位置——尤其当列内卡片高度不一时,靠 clientY 算序号容易误判。
更可靠的做法是监听 dragover 时动态插入一个占位符(div.placeholder),并实时调整它的位置,让用户看到“将插入此处”的视觉反馈。
dragover 中,遍历目标列内所有卡片,用 getBoundingClientRect() 比较 event.clientY 和每个卡片中线,决定 placeholder 插入点pointer-events: none,否则会拦截后续拖拽事件dragover 触发前先移除旧 placeholder,避免残留多个drop)后立即移除 placeholder,再执行真实 DOM 移动iOS / iPadOS 的 Safari 完全不支持 draggable="true" 和相关事件,连 dragstart 都不会触发。强行依赖原生 API 会导致整个功能在 iPhone 上完全不可用。
必须单独检测 'ontouchstart' in window 或 navigator.maxTouchPoints > 0,启用基于 touchstart/touchmove/touchend 的模拟逻辑。
touchstart 记录起始卡片和初始触摸点,禁用默认行为(event.preventDefault())防止滚动touchmove 动态更新卡片位置(transform: translate3d),并实时计算当前列和插入点touchend 触发真实移动,此时需手动触发一次 drop 类似逻辑touchmove 默认会触发页面滚动,务必在绑定时加 { passive: false }
拖拽看似只是“拖过去”,但跨浏览器兼容、跨设备交互、视觉反馈与 DOM 同步这三块,任一环节漏掉细节,用户就会觉得“卡”“不准”“点了没反应”。尤其是 placeholder 的插入时机和 touch 事件的 passive 设置,线上最容易被忽略。