简介:CSS cursor(鼠标样式)是前端开发中非常实用的属性,这份独立 PDF 将 cursor 的常用可选值集中整理成一份速查笔记,面向网页设计、前端开发入门者,也适合需要快速确认光标交互反馈的开发者。内容按用途分类收录了十字准线、手形指针、等待沙漏、帮助问号、文本编辑、可移动对象、八方向尺寸调整、禁止操作、处理中、系统默认以及 URL 自定义光标等常见形态,每个取值都附带中英文对照和语义说明,并特别解释了 hand 与 pointer 的同义写法是为了兼容 IE5、自定义光标需使用 .cur 或 .ani 文件且末尾应补一个通用值等细节,便于读者理解不同样式在按钮、链接、拖拽区域、表格列宽调整等真实场景中的选择依据。资源共 1 个 PDF 文件,压缩包仅 40KB,内容紧凑,适合作为开发时的离线查阅手册。目前已有 742 人学习下载,对需要系统梳理 CSS 光标样式的开发者来说是一份简洁实用的参考资料。
1. 从一次 cursor 事故说起:页面里没人在意的鼠标指针,其实是最便宜的可用性指标
接手一个后台报表系统时,最刺眼的问题不是接口慢,而是表格的行和按钮都在抢鼠标。列表行加了pointer,整行看起来每个格子都能点;底部的导出按钮在请求过程中不置灰,请求结束突然变成not-allowed,用户不知道该不该继续点。查来查去,问题出在cursor的可选值上——这个属性看着简单,但不同交互意图、不同组件状态、不同浏览器实现之间的差异,足以让一套设计稿在真实页面上走样。
cursor做的就是一件事:告诉浏览器,在某个元素上鼠标应该画成什么形状。它解决的是「这里能不能点、是不是在加载、能不能拖拽、能不能选中文本」这类即时反馈问题。对前端工程师、组件库封装者和做可视化大屏的人来说,掌握cursor的完整取值体系、了解url()自定义光标的边界,能让交互反馈在 0 成本前提下明显变专业。下面的内容会先把取值体系梳理清楚,再讲实际页面里最容易出问题的场景,最后给出一套可直接用的防呆方案。
2. CSS cursor 可选值怎么分?先按「交互意图」拆开这套取值体系
2.1 keyword 光标值的四个家族:通用、点击、反馈、拖拽调整
cursor的标准语法很简单:cursor: [ [<url> [<x> <y>]?,]* [ auto | default | none | … ] ]。平时用到的绝大多数是关键字值,CSS 规范里把它们按行为归成几类,记住分类比死记每个拼写更重要。
第一类是「交给浏览器决定」的通用值:auto和default。auto是初始值,浏览器根据元素语义自动选择光标,比如a和button上显示pointer,可编辑区域显示text;default是唯一标准化的显式「箭头」值,注意它不是把光标强制回auto,而是忽略元素语义直接画箭头。
第二类是「点击暗示」:pointer、context-menu、help。pointer表示可点击,是现代 Web 里最常用的值;context-menu表示右键菜单可用,桌面端右键事件较多的表格控件里能用上;help是问号箭头,适合悬停后显示帮助文案的实现。
第三类是「状态反馈」:wait、progress、not-allowed、no-drop、crosshair、cell、text、vertical-text。其中wait是漏斗或沙漏,progress在大多数系统里是「带箭头的漏斗」,表示后台还在忙但页面可交互,这两个是加载场景的主力。text是文本编辑时的 I 形指针,vertical-text是竖排文本的 I 形指针,中文古籍排版或 HTML 竖排文章里能用到。
第四类是「拖拽和调整」:grab、grabbing、move、all-scroll、alias、copy,以及col-resize、row-resize、n-resize、ne-resize这一整组方向尺寸调整值。做卡片拖拽排序、侧边栏拖宽、表格列宽调整时会碰见它们。alias在 Mac 上是带小箭头的别名图标,多数浏览器把它实现成「快捷方式」样式,copy是带加号的复制光标,在拖拽文件或列表项复制场景里很有用。
2.2 常用关键字与语义对照表:直接当设计规范用
| 属性值 | 外观示意 | 交互含义 | 适用场景 | 兼容性 |
|---|---|---|---|---|
auto | 系统默认 | 按元素语义自动 | 不干预时 | 全平台 |
default | 箭头 | 普通定位 | 非交互元素 | 全平台 |
pointer | 手型 | 可点击 | 链接、按钮、可点行 | 全平台 |
help | 问号箭头 | 鼠标悬停有辅助说明 | 带 tooltip 的图标 | 全平台 |
wait | 沙漏 / 转圈 | 页面或任务忙 | 同步阻塞 | 全平台 |
progress | 漏斗带箭头 | 后台进行中可交互 | 异步加载 | 全平台 |
text | I 形 | 可编辑文本 | 输入框、富文本 | 全平台 |
vertical-text | 横向 I 形 | 竖排文本编辑 | 竖排文章、古籍页 | 桌面端 |
cell | 十字粗线 | 可选中单元格 | 表格数据选中 | Chrome/Edge |
crosshair | 精准十字 | 精确取点 | 截图、图表取坐标 | 全平台 |
move | 四向箭头 | 可移动 | 拖拽容器 | 全平台 |
grab | 手半握 | 可抓取 | 滑块、地图拖拽 | 现代浏览器 |
grabbing | 手捏紧 | 正在抓取 | 拖拽过程 | 现代浏览器 |
all-scroll | 四向箭头 | 可平移 | 画布拖拽 | 全平台 |
col-resize | 左右箭头 | 列宽调整 | 表格列、分栏拖宽 | 全平台 |
row-resize | 上下箭头 | 行高调整 | 表格行高 | 全平台 |
no-drop | 禁止圆圈 | 不能放置 | 拖放目标非法 | 全平台 |
not-allowed | 圆中斜杠 | 操作被禁止 | 禁用按钮、禁点项 | 全平台 |
alias | 快捷方式箭头 | 创建别名链接 | 拖拽复制快捷方式 | 桌面端 |
copy | 箭头加号 | 复制到目标 | 拖拽复制 | 桌面端 |
none | 隐藏指针 | 无指针 | 全屏演示、canvas 游戏 | 全平台 |
表里有个容易被忽略的点:pointer不等于「可点击」,它只是「看起来可点击」的暗示。如果某个元素加了cursor: pointer却没有绑定任何点击行为,用户点下去毫无反应,体验比重置成default更糟。这也是后面第三四章专门讲「cursor 和事件要联动」的原因。
2.3 用 Chrome DevTools 临时改光标值验证效果
判断一个值在当前浏览器里长什么样,最快的办法不是翻文档,而是用 DevTools 实时改。在 Elements 面板选中目标元素,右侧 Styles 里找到cursor声明后直接双击改值,鼠标移到元素上立刻能看到变化。但 DevTools 只能改当前元素,想系统看一遍全部可选值是低效的。常见做法是直接在浏览器控制台里跑一段脚本,枚举所有关键字并依次展示:
const cursorValues = [ 'auto', 'default', 'none', 'pointer', 'context-menu', 'help', 'wait', 'progress', 'cell', 'crosshair', 'text', 'vertical-text', 'alias', 'copy', 'move', 'no-drop', 'not-allowed', 'grab', 'grabbing', 'all-scroll', 'col-resize', 'row-resize', 'n-resize', 'ne-resize', 'e-resize', 'se-resize', 's-resize', 'sw-resize', 'w-resize', 'nw-resize' ]; const demoDiv = document.createElement('div'); demoDiv.style.cssText = 'position:fixed;left:50%;top:50%;transform:translate(-50%,-50%);width:200px;height:120px;display:flex;align-items:center;justify-content:center;background:#f0f0f0;border:1px solid #ccc;z-index:99999;font:14px/1.5 sans-serif;'; document.body.appendChild(demoDiv); cursorValues.forEach((value, index) => { setTimeout(() => { demoDiv.style.cursor = value; demoDiv.textContent = `cursor: ${value}`; }, index * 800); });代码逻辑是把所有样式值依次应用到页面中央的固定定位元素上,每 800 毫秒切换一个值并显示当前名字。这样能在两分钟内把整个取值体系过一遍。严格说,这里cell、vertical-text等值在 Firefox 或部分 Linux 桌面环境里会被回退成default,这是系统级光标主题决定的,不是 CSS 写错。
3. cursor 在实际页面里最容易踩的坑:disabled、click 与 pointer-events 三者的关系
3.1 disabled 按钮的 cursor 为什么会失效?pointer-events 在中间捣了什么乱
最常见的写法是这样:
<button class="primary" disabled>保存</button>button.primary[disabled] { cursor: not-allowed; }在大多数浏览器里,这个not-allowed不会生效,鼠标经过禁用按钮时仍然显示普通箭头。原因是很多现代 UI 框架里disabled按钮同时带上了pointer-events: none,而pointer-events: none会让元素从命中测试里完全消失,鼠标事件、CSS 悬停状态,以及cursor全部失效。这是开发人员最常遇到的「我明明写了 cursor 为什么不显示」的案例之一。
处理这个问题有两个思路。第一个思路是保留disabled属性,改用not-allowed以外的反馈方式——比如用aria-disabled="true"配合tabindex="-1"模拟禁用,而不是让元素彻底脱离事件流;第二个思路是把 cursor 写到外层包装元素上,让外层承担光标样式:
<span class="btn-wrapper"> <button class="primary" disabled>保存</button> </span>.btn-wrapper { position: relative; } .btn-wrapper[aria-disabled="true"], .btn-wrapper:has(button:disabled) { cursor: not-allowed; }cursor是继承属性,父元素的光标会传到子元素。但要注意,当按钮自身设置了cursor时,继承关系被打断;按钮没有显式设置cursor时,父元素的not-allowed才能透传下来。这里用:has()判断按钮是否 disabled,是较新的 CSS 选择器能力,老项目里更保险的做法是 wrapper 上直接由 Vue/React 根据状态绑定一个 class。
3.2 用 JS 动态切换 cursor:加载态、提交成功、等待反馈一块演出
页面上的按钮点击后通常要经历「可点击 → 加载中 → 结果反馈」三个阶段,cursor 也应跟着走一遍:
<button id="payBtn" class="primary">立即支付</button>const payBtn = document.getElementById('payBtn'); let isSubmitting = false; payBtn.addEventListener('click', async () => { if (isSubmitting) return; isSubmitting = true; payBtn.disabled = true; payBtn.style.cursor = 'progress'; payBtn.textContent = '处理中…'; try { const result = await fakePayRequest(); if (result.success) { payBtn.textContent = '支付成功'; payBtn.style.cursor = 'default'; } else { payBtn.textContent = '重试'; payBtn.disabled = false; payBtn.style.cursor = 'pointer'; } } finally { isSubmitting = false; } });这里用的是progress而不是wait,含义差异在于:wait表示整个页面阻塞不可操作,progress表示后台任务进行中但用户还能做别的事。支付按钮场景其实是「只锁当前按钮,页面其余部分可点」,语义上progress更准确。这也是 cursor 可选值里最容易被混用的两个值。
注意一个细节:代码里payBtn.style.cursor = 'progress'是内联样式,它的优先级高于任何样式表里的cursor: pointer,所以能保证状态切换时不被全局样式覆盖。组件库封装时如果遇到按钮样式优先级打架,内联是关键手段。
3.3 iframe、键盘用户与 touch 设备:cursor 管不到的地方
设置cursor: pointer的位置,无论事件绑定写得多标准,有三类场景是 handle 不到的。
第一类是 iframe 边界。iframe 内部和外部是两个独立的文档,光标样式只在鼠标位于 iframe 内容区域内时,由 iframe 内的 CSS 决定;鼠标从页面移到 iframe 上时,iframe 边界上显示的是父页面设置的光标。做低代码平台拖拽组件进 iframe 的画布时,常会看到光标在 iframe 边缘「跳变」,这是文档边界导致的,不是 bug。
第二类是键盘用户。Tab 键聚焦元素,屏幕上出现的焦点环由:focus-visible控制,和cursor完全不相干。一个元素即使没有cursor: pointer,键盘用户照样能通过 Tab 访问并回车触发点击;反过来,cursor: pointer也不会让一个 div 变成可聚焦元素。可点击区域必须同时保证可以 Tab 到达、能按回车触发,这是键盘可达性要求,cursor 在这里只是视觉辅助。
第三类是触摸设备。触摸屏没有鼠标指针,CSS cursor 完全不生效,但它在移动端仍然有意义——现在的触控设备可以外接鼠标或触控板,iPadOS 的鼠标指针会读取网页里的cursor值来改变悬停形状,这也解释了为什么移动端页面里写不写 cursor 会直接影响外接配件的使用体验。
4. 自定义 cursor:url() 的语法、坐标与兼容性上限
4.1 url() + 双坐标的完整语法,以及为什么 auto 兜底必须写
关键字值能满足大多数需求,但做编辑器、游戏或品牌官网时会想用自定义光标。语法是url(光标的文件或 data URI) x y, 兜底关键字,其中x y是热点坐标,表示光标的实际触发点:
.editor-canvas { cursor: url('/assets/cursors/pen.svg') 2 18, default; }这段代码里的2 18是热点坐标,单位是像素,相对于图片左上角。如果不写坐标,浏览器默认用图片左上角作为热点;而 SVG 光标左上角往往是空白的,不用坐标就会感觉光标对不准,绘图和取色类工具尤其明显。坐标值可以直接写数值,不需要单位。
真正影响自定义光标的,是浏览器对图片格式和大小的限制。Chrome 和 Firefox 修改过好几轮上限,最常见的历史约束是最大 32x32 像素,超过尺寸的图片会被直接忽略并回退到兜底关键字。现在新版本对 128x128 以内的大光标支持逐步放开,但为了兼容旧版桌面浏览器,生产环境仍然按 32x32 做最稳。格式方面,Chrome 支持 PNG、GIF、SVG 以及内联的 data URI,IE 只支持 CUR/ANI 格式,这个已经是历史包袱,但现在仍有一定存量用户需要考虑。
4.2 用 SVG 制作 32 像素热点准确的光标:一段可直接用的代码
SVG 是当前做自定义光标最好的方案,矢量不模糊,还能直接内联成 data URI,省一次网络请求。先做一个 32x32 的画笔光标:
<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"> <path d="M4 28 L12 6 L20 14 L28 4 L32 8 L24 18 L16 10 Z" fill="#222" stroke="#ffffff" stroke-width="1.5" stroke-linejoin="round"/> </svg>SVG 里的画笔路径是一个从左上角延伸的箭头形状,白色描边是为了在深色背景上仍然可见。这个 SVG 可以直接编码成 Base64 后嵌进url(),也可以用encodeURIComponent编码后内联:
.custom-cursor { cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><path d="M4 28 L12 6 L20 14 L28 4 L32 8 L24 18 L16 10 Z" fill="%23222" stroke="%23ffffff" stroke-width="1.5" stroke-linejoin="round"/></svg>') 6 4, default; }注意 SVG 里的#符号在 URL 里必须编码成%23,否则会被解析成 URL 片段标识符,导致图片加载失败。原点6 4是笔尖位置,对应坐标系里的触达点。另一个细节是 SVG 需要显式写widthheight和viewBox,因为部分浏览器在解析 data URI 里的 SVG 时,如果尺寸信息缺失会把图像当 0x0 处理。
4.3 自定义光标的兼容性回落写法:从渐变到稳定降级
自定义光标最大的问题是——如果图片加载失败、格式不支持或尺寸超限,整个声明就失效。所以 URL 后面的兜底关键字不是可选配置,是保证可用性的底线。生产代码里我的习惯是写两段:
.locked-layer { cursor: not-allowed; } .locked-layer.custom { cursor: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" width="32" height="32" viewBox="0 0 32 32"><circle cx="16" cy="16" r="14" fill="none" stroke="%23d33" stroke-width="2"/><line x1="8" y1="8" x2="24" y2="24" stroke="%23d33" stroke-width="2"/></svg>') 16 16, not-allowed; }第一行给不支持自定义光标的旧环境一个基础样式;第二行里热点的16 16正好是圆的中心,斜杠交点与视觉禁止符号的交叉点对齐,鼠标触达时不会出现「感觉没碰到」的偏差。
还要提一个细节:url()后面可以接多个 URL 作为逐级回退,浏览器会尝试加载第一个,失败则加载第二个,最终落到关键字:
.cursor-multi { cursor: url('fallback.cur') 4 4, url('main.svg') 4 4, pointer; }这种多级写法对同时兼容 IE 和现代浏览器的场景很实用:.cur放前面,IE 能用,Chrome 也能加载;.svg放第二位,现代浏览器如果第一个加载失败还会继续尝试。但注意不要把坐标写在关键字后面——浏览器对这种语法直接判废。
5. 给 cursor 写一套「防呆」基础样式库,顺手做成清单
5.1 统一维护 cursor 变量:避免在组件里裸写 pointer 和 not-allowed
大型项目里最常见的 cursor 问题是「标准不统一」:有的组件cursor: pointer,有的组件写成cursor: default但期望点击,还有直接把内联样式散落进 JSX 和 Vue 模板里。维护一套通用 class 或 CSS 自定义属性,比全局搜索替换要省力得多。
我一般会定义一组语义化的 class 清单,放在全局样式文件里:
.u-cursor-auto { cursor: auto; } .u-cursor-pointer { cursor: pointer; } .u-cursor-text { cursor: text; } .u-cursor-wait { cursor: wait; } .u-cursor-progress { cursor: progress; } .u-cursor-move { cursor: move; } .u-cursor-grab { cursor: grab; } .u-cursor-grabbing { cursor: grabbing; } .u-cursor-not-allowed { cursor: not-allowed; } .u-cursor-copy { cursor: copy; } .u-cursor-alias { cursor: alias; }如果项目用的是 CSS Modules 或 Tailwind 之类方案,不一定要照抄这套 class,但核心思想一致:不要在业务 CSS 里直接写cursor: pointer,围绕「交互意图」定义统一的 token。这是因为当某个浏览器内核更新、某类交互语义调整时,统一在一个文件里改比散布在几十个组件里改要快得多。
5.2 交互组件光标的快速自查清单
| 组件状态 | 应使用的 cursor | 常见错误 |
|---|---|---|
| 普通文本段落 | auto或text | 写pointer |
| 普通文本段落但有 tooltip | help | 写pointer或漏写 |
| 链接、按钮 | pointer | 用default或auto覆盖 |
| 禁用按钮 | not-allowed+aria-disabled | 设disabled后 cursor 失效 |
| 表格行(点击行跳转) | pointer | 行内过于密集的手型反馈 |
| 拖拽把手 | grab,拖拽中grabbing | 写move(含义是自由移动,不表达抓取) |
| 调整列宽 | col-resize | 写n-resize(方向错误) |
| 加载中按钮 | progress | 写wait(语义过重) |
| 不可放置的拖放目标 | no-drop | 写not-allowed |
自查逻辑很简单:先问这个元素有没有点击/拖拽行为,若没有,就不该有pointer和grab;再问操作是否可执行,不可执行时用not-allowed或no-drop;最后问后台状态是阻塞交互还是允许继续操作,用wait和progress区分。「鼠标指针的形状应当反映下一步能做什么」一条准则,能解决 90% 的光标设置错误。
5.3 把这份 cursor 清单变成可验证文件:一份浏览器控制台轮播脚本
最后给出一个实用技巧:把上面提到的所有值和设计规范整理成一个常量数组,放在项目公共文件里,配合一个简单脚本,在任何页面里按空格切换。这样设计走查时不用一个个手动改 DevTools,截图对比也方便:
// cursor-spec.js const CURSOR_SPEC = [ { label: 'default-arrow', value: 'default', note: '普通定位' }, { label: 'clickable', value: 'pointer', note: '可点击' }, { label: 'text-edit', value: 'text', note: '文本编辑' }, { label: 'loading-block', value: 'wait', note: '全局阻塞' }, { label: 'loading-active', value: 'progress', note: '后台进行' }, { label: 'move-free', value: 'move', note: '自由移动' }, { label: 'drag-grab', value: 'grab', note: '可抓取' }, { label: 'drag-holding', value: 'grabbing', note: '抓取中' }, { label: 'forbidden', value: 'not-allowed', note: '禁止操作' }, { label: 'drop-forbidden', value: 'no-drop', note: '禁止拖放' }, { label: 'resize-col', value: 'col-resize', note: '调整列宽' }, { label: 'help-hover', value: 'help', note: '帮助提示' } ]; let currentIndex = 0; document.addEventListener('keydown', (e) => { if (e.code !== 'Space') return; e.preventDefault(); const spec = CURSOR_SPEC[currentIndex % CURSOR_SPEC.length]; const el = document.createElement('div'); el.style.cssText = `position:fixed;left:50%;top:50%;transform:translate(-50%,-50%); width:240px;height:140px;background:#222;color:#fff;display:flex; align-items:center;justify-content:center;border-radius:8px; font:15px/1.6 system-ui,sans-serif;z-index:999999; cursor:${spec.value}; pointer-events:none;`; el.textContent = `${spec.label}(${spec.value}) · ${spec.note}`; document.body.appendChild(el); setTimeout(() => el.remove(), 1200); currentIndex++; });按下空格就在屏幕中央显示一个带当前cursor值的半透明浮层,1.2 秒后自动消失,下一次按键继续轮播。这个脚本不需要引入任何库,直接粘贴到控制台即可运行。它不只是演示工具,还能帮团队在评审一个页面时快速检查「我当前鼠标悬停的控件到底用的什么光标」。把CURSOR_SPEC数组导出成 JSON,还能在组件单元测试里遍历校验每个交互组件的 cursor 是否严格按照规范绑定,避免回归问题。
本文还有配套的精品资源,点击获取