用纯 CSS 做个提示气泡,不用引任何库
鼠标停在一个图标上,弹出一行小字说明——这种 Tooltip 几乎每个项目都要用。很多人第一反应是引个 UI 库,但其实纯 CSS 十几行就能搞定,还能顺便做个小箭头。
这篇讲完整实现,以及几个"做出来发现有毛病"的常见问题。
一、最简版本
用一个自定义属性存提示内容,再用伪元素把它显示出来:
<button class="tip" data-tip="删除后不可恢复">删除</button>
.tip {
position: relative; /* 关键:给伪元素提供定位参照 */
}
.tip::after {
content: attr(data-tip); /* 把属性值当作内容显示 */
position: absolute;
bottom: 100%; /* 贴在元素上方 */
left: 50%;
transform: translateX(-50%);
margin-bottom: 8px;
background: #2f3a4d;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0; /* 默认隐藏 */
pointer-events: none; /* 不挡鼠标 */
transition: opacity .15s;
}
.tip:hover::after {
opacity: 1; /* 悬停时显示 */
}
几个关键点:
content: attr(data-tip)是核心,直接把 HTML 属性当作伪元素的内容,不用在 CSS 里写死文案。position: relative加在宿主元素上,否则伪元素的absolute会相对更外层的定位元素跑掉。pointer-events: none必须加,不然伪元素虽然透明但仍会拦截鼠标事件,可能导致 hover 状态闪烁。- 用
opacity而不是display,因为display没法做过渡动画。
二、加个小箭头
用另一个伪元素 ::before 画个三角形:
.tip::before {
content: '';
position: absolute;
bottom: 100%;
left: 50%;
transform: translateX(-50%);
margin-bottom: 2px;
/* 用边框画三角形 */
border: 6px solid transparent;
border-top-color: #2f3a4d;
opacity: 0;
pointer-events: none;
transition: opacity .15s;
}
.tip:hover::before {
opacity: 1;
}
三角形是 CSS 的经典技巧:一个宽高为 0 的元素,给四条边框不同颜色,相邻边框的交界就是斜线。这里只要顶边有颜色、其余透明,就得到一个朝下的三角。
三、换个方向
上面的提示在元素上方。想要下方、左方、右方,改定位就行:
/* 下方 */
.tip-bottom::after {
bottom: auto;
top: 100%;
margin-bottom: 0;
margin-top: 8px;
}
.tip-bottom::before {
bottom: auto;
top: 100%;
margin: 0;
margin-top: 2px;
border-top-color: transparent;
border-bottom-color: #2f3a4d;
}
一般我会定义四个方向的 class(.tip-top / .tip-bottom / .tip-left / .tip-right),用的时候选一个。
纯 CSS 方案有四个绕不开的限制,心里要有数:
| 限制 | 表现 | 应对 |
|---|---|---|
| 被裁剪 | 祖先有 overflow: hidden 时只显示一半 |
换方向,或改用 JS 方案 |
| 超出屏幕 | 贴右边的元素,提示溢出看不见 | 边缘元素改用侧向,或限制宽度换行 |
| 触屏无效 | 手机上没有 hover | 提示只能是补充信息 |
| 读屏读不到 | 伪元素不在 DOM 里 | 同时加 aria-label 或 title |
四、第一个坑:被父元素裁剪
做出来发现提示只显示一半、或者完全看不见——多半是某个祖先元素设了 overflow: hidden。
伪元素是绝对定位的,会超出宿主元素的范围,祖先的 overflow: hidden 会把超出的部分裁掉。
几种解法:
- 如果那个
overflow: hidden只是用来清除浮动或裁剪圆角,换成overflow: clip试试(有些场景不影响)。 - 把提示改成朝另一个方向(比如改到下方就不被上边界裁了)。
- 实在不行只能用 JS 方案:把提示元素挪到
<body>下,用 JS 计算位置。这是 UI 库的做法,但引入了复杂度。
五、第二个坑:超出屏幕边界
元素在页面最右边时,居中的提示会溢出屏幕,右边那截看不见。
纯 CSS 没有完美的自适应解法,几个折中:
-
边缘的元素单独用
.tip-left(提示朝左展开)。 -
限制宽度让它换行:
.tip::after { max-width: 200px; white-space: normal; /* 允许换行 */ text-align: left; } -
或者干脆接受这个限制——大多数情况下元素不会正好贴边。
六、第三个坑:触屏上没有 hover
手机上没有悬停状态,所以纯 CSS 的 hover 提示在触屏设备上基本不生效(部分浏览器点一下会触发一次 hover,行为很不一致)。
所以:
- 提示内容只能是补充信息,不能是必需信息。用户看不到也得能正常用。
- 关键信息别放 tooltip 里,直接显示出来或者用可展开的说明。
- 真要支持触屏点击弹出,那就得加 JS 了。
七、第四个坑:屏幕阅读器读不到
::after 生成的内容不在 DOM 里,大多数屏幕阅读器读不到(虽然规范上应该读,但实现参差不齐)。
所以正确做法是:同时用 aria-label 或 title 提供一份同样的信息。
<button class="tip" data-tip="删除后不可恢复" aria-label="删除(不可恢复)">
删除
</button>
或者更简单——直接用原生 title 属性做兜底,浏览器会显示系统默认的提示(虽然样式不好看、还有延迟,但无障碍是好的)。
几个补充
-
别在提示里放交互元素。纯 CSS 的 tooltip 没法点,放链接、按钮都没用(而且
pointer-events: none挡着)。 -
内容长的话别用属性。超长文案塞在 HTML 属性里很难维护,那种场景考虑用子元素 + JS 方案。
-
z-index要给够。提示要盖在其他内容上面,但注意别盖住更重要的浮层(比如弹窗)。 -
延迟显示。鼠标扫过就立刻弹出会有点烦,可以加个延迟:
.tip::after { transition: opacity .15s .4s; /* 延迟 0.4 秒再出现 */ } .tip:hover::after { transition-delay: 0s; /* 移出时立刻消失?其实这里是移入时无延迟 */ }嗯这个写法要小心,通常想要的是"移入延迟、移出立即",写法是默认状态设延迟、hover 状态设 0。
小结
纯 CSS 的 Tooltip 实现成本很低——data-tip 属性存内容、::after 显示、::before 画三角、hover 控制 opacity,加起来二十行 CSS,不用引任何库。但要知道它的边界:祖先有 overflow 会被裁剪、贴边元素会溢出屏幕、触屏上无效、屏幕阅读器读不到。所以定位应该是"锦上添花的补充说明",关键信息别依赖它,同时用 aria-label 补一份给辅助技术。
- 点赞
- 收藏
- 关注作者
评论(0)