必须用外层未缩放容器代理事件并动态换算坐标,因clientX/clientY基于缩放前布局盒计算,直接缩放会导致点击区域偏移;需解析transform矩阵获取scale,结合getBoundingClientRect和transform-origin修正坐标,并为移动端设置touch-action及统一处理touch/click事件。

直接用 transform: scale() 缩放容器,点击区域必然偏移——这不是 bug,是浏览器事件坐标系统与视觉渲染分离的固有行为。必须绕过它,而不是试图“修”它。
为什么 clientX/clientY 不随 scale 变化
浏览器派发 click 事件时,clientX 和 clientY 始终基于元素缩放前的原始布局盒(layout box)计算。哪怕你把一个 div 缩到 0.5 倍,它的 getBoundingClientRect().width 还是 200px,事件坐标也仍按这 200×100 的框来映射。视觉上你点在左上角,实际触发的是缩放前坐标 (10,10),换算到缩放后就是 (20,20) —— 偏移就出来了。
常见现象包括:echarts 图表点击错位、canvas 拾取失效、自定义 tooltip 定位飘移、移动端 touch 无响应。
用外层容器代理事件 + 动态换算坐标
最稳妥的做法是:把交互逻辑从缩放元素本身剥离,交给一个未应用 transform 的父容器处理。
立即学习“前端免费学习笔记(深入)”;
- 给缩放内容包一层
<div class="click-proxy">,该容器不设transform,仅负责捕获事件 - 缩放样式只加在内层子元素上(如
.chart-container) - 所有监听绑定在
.click-proxy上,再手动换算坐标
关键代码:
const proxy = document.querySelector('.click-proxy');
proxy.addEventListener('click', (e) => {
const rect = proxy.getBoundingClientRect();
// 动态读取 scale,不能硬编码
const style = window.getComputedStyle(proxy.firstElementChild);
const matrix = new DOMMatrix(style.transform);
const scale = Math.abs(matrix.a); // 2D scale X
const x = (e.clientX - rect.left) / scale;
const y = (e.clientY - rect.top) / scale;
chart.dispatchAction({ type: 'click', x, y });
});
注意:transform-origin 若不是 0 0,需额外减去中心偏移再还原;否则换算结果仍会偏。
移动端 touch 事件要额外加两道保险
iOS Safari 和部分 Android 浏览器中,仅靠 JS 换算还不够:
- 必须设置
touch-action: none或touch-action: manipulation,否则默认手势(如双指缩放)可能拦截touchstart -
pointer-events: auto要确保没被父级pointer-events: none覆盖 - 推荐同时监听
touchstart和click,并统一走同一套换算逻辑,避免 mouse/touch 行为不一致
漏掉任意一项,比如忘了 touch-action,在真机上就可能完全点不动。
真正麻烦的从来不是换算公式
是缩放因子来源不可控:scale 可能来自 CSS 变量、JS 动态设置、媒体查询响应式规则,甚至嵌套多层 transform。每次都要解析 transform 矩阵;transform-origin 混合影响会让偏移方向和大小都难预测;而移动端 touch 和 mouse 事件路径不同,又得单独适配。这些细节只要漏一个,偏移就回来了。



















