直接用title属性无法实现真正气泡框,因其无箭头、不可自定义样式、移动端基本失效;纯CSS气泡需用data-*属性存文案、::before/::after画箭头与气泡、position:relative+absolute精确定位,并配合opacity+visibility实现过渡动画。

直接用 title 属性最省事,但别指望它能“弹出”真正意义上的气泡框
浏览器原生的 title 属性确实能让鼠标悬停时显示浅灰色小提示,但它不是“气泡框”——没有箭头、不能自定义颜色/圆角/字体、延迟固定约 500ms、移动端基本不触发(iOS Safari 默认禁用),而且一旦父容器有 overflow: hidden 或 pointer-events: none,就立刻失效。
常见误用场景:
- 给
<button></button>外层包裹的<div> 加 <code>title,结果没反应——title必须加在可交互的元素本身上,比如<button title="保存"></button> - 表格中给
<table> 加 <code>title,期望整表都有提示——无效,title不继承,得加到具体<td> 或 <code><th> <li>内容含换行或 HTML 标签,比如 <code>title="第一行<br>第二行"——浏览器会原样显示标签字符,不解析
纯 CSS 实现带箭头的气泡框,关键靠 ::before + ::after 和定位
不需要 JS 就能做出带三角箭头、圆角、背景色的气泡,核心是利用属性存文案、伪元素画气泡和箭头、绝对定位对齐。但要注意:必须给触发元素设 position: relative,否则气泡会相对于 body 定位,飘走。
典型结构示例:
立即学习“前端免费学习笔记(深入)”;
<button class="tooltip-trigger" data-tooltip="确认删除?不可恢复">删除</button>
<p><style>
.tooltip-trigger {
position: relative;
}
.tooltip-trigger::after {
content: attr(data-tooltip);
position: absolute;
bottom: 125%;
left: 50%;
transform: translateX(-50%);
background: #333;
color: #fff;
padding: 4px 8px;
border-radius: 4px;
font-size: 12px;
white-space: nowrap;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.tooltip-trigger::before {
content: "";
position: absolute;
bottom: 100%;
left: 50%;
margin-left: -4px;
border-width: 4px 4px 0 4px;
border-style: solid;
border-color: #333 transparent transparent transparent;
opacity: 0;
visibility: hidden;
transition: opacity 0.2s, visibility 0.2s;
}
.tooltip-trigger:hover::after,
.tooltip-trigger:hover::before {
opacity: 1;
visibility: visible;
}
</style>几个容易踩的坑:
-
transform: translateX(-50%)是让气泡水平居中对准按钮中心,漏写会导致偏移 - 伪元素的
opacity+visibility必须一起控制,只用display: none会破坏 transition 动画 - 移动端 hover 不可靠,这个方案在 iOS 上基本不生效,别在生产环境单独依赖它
要真正在移动端生效,得用 JavaScript 监听 mouseenter / focus 和 touchstart
纯 CSS 的 hover 在手机上无法触发,必须靠 JS 补全交互链路。关键是把 touchstart 当作首次点击来处理,避免双击放大等干扰;同时保留 focus 支持键盘用户(比如 Tab 导航到按钮后按 Enter)。
最小可行 JS 示例(不依赖框架):
document.querySelectorAll('[data-tooltip]').forEach(el => {
const tooltipText = el.getAttribute('data-tooltip');
let timeoutId;
<p>const show = () => {
// 创建气泡元素并插入
const tip = document.createElement('div');
tip.className = 'js-tooltip';
tip.textContent = tooltipText;
tip.style.position = 'absolute';
document.body.appendChild(tip);</p><pre class="brush:php;toolbar:false;">// 计算位置(简化版:放在元素下方居中)
const rect = el.getBoundingClientRect();
tip.style.left = `${rect.left + rect.width / 2 - tip.offsetWidth / 2}px`;
tip.style.top = `${rect.bottom + 8}px`;
// 清除旧定时器,防止快速进出多次创建
clearTimeout(timeoutId);};
const hide = () => { const tip = document.querySelector('.js-tooltip'); if (tip) tip.remove(); };
el.addEventListener('mouseenter', show); el.addEventListener('focus', show); el.addEventListener('touchstart', e => { e.preventDefault(); // 防止 touch 导致 focus 冲突 show(); });
el.addEventListener('mouseleave', () => { timeoutId = setTimeout(hide, 300); // 离开后延迟隐藏,防误触 }); el.addEventListener('blur', hide); el.addEventListener('touchend', hide); });
注意点:
- 不要用
click替代touchstart,会有约 300ms 延迟,体验卡顿 - 计算位置时用
getBoundingClientRect()而非offsetTop,后者不包含滚动偏移 - 务必加
e.preventDefault()在touchstart里,否则 iOS 可能触发两次事件或缩放
复杂项目建议直接用 Popper.js 或 UI 库的 Tooltip 组件
自己手写要考虑滚动时气泡跟随、视口边界检测(比如按钮在右下角,气泡不能被切掉)、z-index 层级、动画帧率、键盘导航(Esc 关闭)、焦点管理(Tab 进入气泡内容)……这些逻辑 Popper.js 已经封装好,主流 UI 库如 Ant Design 的 Tooltip 也是基于它。
如果你用的是 React,一行就能启用:
<Tooltip title="这是自定义气泡"> <Button>悬停我</Button> </Tooltip>
真正容易被忽略的点是:气泡内容若含交互(比如链接、按钮),必须手动设置 tabIndex="0" 并监听 keydown 处理 Esc 关闭,否则键盘用户无法操作——这点几乎所有手写方案都会漏掉。



















