Skip to content

工具提示 Tooltip

工具提示是鼠标悬停时显示的信息提示组件。

基础用法

基础工具提示

html
<button class="glass-button" data-tooltip="这是一个提示信息">
  悬停查看提示
</button>

不同位置

html
<!-- 上方 -->
<button class="glass-button" data-tooltip="上方提示" data-tooltip-position="top">
  上方提示
</button>

<!-- 下方 -->
<button class="glass-button" data-tooltip="下方提示" data-tooltip-position="bottom">
  下方提示
</button>

<!-- 左侧 -->
<button class="glass-button" data-tooltip="左侧提示" data-tooltip-position="left">
  左侧提示
</button>

<!-- 右侧 -->
<button class="glass-button" data-tooltip="右侧提示" data-tooltip-position="right">
  右侧提示
</button>

工具提示内容

纯文本提示

html
<span class="tooltip-trigger" data-tooltip="这是纯文本提示">
  文本提示
</span>

富文本提示

html
<div class="tooltip-trigger" data-tooltip-html="true">
  <span>富文本提示</span>
  <div class="glass-tooltip">
    <strong>标题</strong><br>
    这是包含<em>HTML</em>的提示内容
  </div>
</div>

延迟显示

html
<button class="glass-button" 
        data-tooltip="延迟500ms显示" 
        data-tooltip-delay="500">
  延迟提示
</button>

工具提示触发方式

鼠标悬停触发

html
<button class="glass-button" data-tooltip="鼠标悬停显示" data-tooltip-trigger="hover">
  悬停触发
</button>

点击触发

html
<button class="glass-button" data-tooltip="点击显示/隐藏" data-tooltip-trigger="click">
  点击触发
</button>

焦点触发

html
<input type="text" 
       class="glass-input" 
       placeholder="获得焦点时显示提示"
       data-tooltip="请输入有效的邮箱地址" 
       data-tooltip-trigger="focus">

CSS 类名

基础类名

  • .glass-tooltip - 工具提示容器
  • .tooltip-trigger - 触发元素
  • .tooltip-arrow - 提示箭头

修饰类名

位置修饰符

  • .top - 上方显示
  • .bottom - 下方显示
  • .left - 左侧显示
  • .right - 右侧显示

状态修饰符

  • .show - 显示状态

JavaScript 增强

工具提示管理器

javascript
class TooltipManager {
  constructor() {
    this.tooltips = new Map();
    this.init();
  }
  
  init() {
    this.bindEvents();
  }
  
  bindEvents() {
    document.addEventListener('mouseenter', (e) => {
      const trigger = e.target.closest('[data-tooltip]');
      if (trigger && this.getTriggerType(trigger) === 'hover') {
        this.show(trigger);
      }
    }, true);
    
    document.addEventListener('mouseleave', (e) => {
      const trigger = e.target.closest('[data-tooltip]');
      if (trigger && this.getTriggerType(trigger) === 'hover') {
        this.hide(trigger);
      }
    }, true);
    
    document.addEventListener('click', (e) => {
      const trigger = e.target.closest('[data-tooltip]');
      if (trigger && this.getTriggerType(trigger) === 'click') {
        this.toggle(trigger);
      }
    });
    
    document.addEventListener('focus', (e) => {
      const trigger = e.target.closest('[data-tooltip]');
      if (trigger && this.getTriggerType(trigger) === 'focus') {
        this.show(trigger);
      }
    }, true);
    
    document.addEventListener('blur', (e) => {
      const trigger = e.target.closest('[data-tooltip]');
      if (trigger && this.getTriggerType(trigger) === 'focus') {
        this.hide(trigger);
      }
    }, true);
  }
  
  getTriggerType(element) {
    return element.dataset.tooltipTrigger || 'hover';
  }
  
  show(trigger) {
    const delay = parseInt(trigger.dataset.tooltipDelay) || 0;
    
    if (delay > 0) {
      setTimeout(() => {
        this.createTooltip(trigger);
      }, delay);
    } else {
      this.createTooltip(trigger);
    }
  }
  
  hide(trigger) {
    const tooltip = this.tooltips.get(trigger);
    if (tooltip) {
      tooltip.classList.remove('show');
      setTimeout(() => {
        if (tooltip.parentNode) {
          tooltip.parentNode.removeChild(tooltip);
        }
        this.tooltips.delete(trigger);
      }, 200);
    }
  }
  
  toggle(trigger) {
    if (this.tooltips.has(trigger)) {
      this.hide(trigger);
    } else {
      this.show(trigger);
    }
  }
  
  createTooltip(trigger) {
    if (this.tooltips.has(trigger)) return;
    
    const content = trigger.dataset.tooltip;
    const position = trigger.dataset.tooltipPosition || 'top';
    const isHtml = trigger.dataset.tooltipHtml === 'true';
    
    const tooltip = document.createElement('div');
    tooltip.className = `glass-tooltip ${position}`;
    
    if (isHtml) {
      tooltip.innerHTML = trigger.querySelector('.glass-tooltip').innerHTML;
    } else {
      tooltip.textContent = content;
    }
    
    // 添加箭头
    const arrow = document.createElement('div');
    arrow.className = 'tooltip-arrow';
    tooltip.appendChild(arrow);
    
    document.body.appendChild(tooltip);
    this.tooltips.set(trigger, tooltip);
    
    // 计算位置
    this.positionTooltip(trigger, tooltip, position);
    
    // 显示动画
    setTimeout(() => {
      tooltip.classList.add('show');
    }, 10);
  }
  
  positionTooltip(trigger, tooltip, position) {
    const triggerRect = trigger.getBoundingClientRect();
    const tooltipRect = tooltip.getBoundingClientRect();
    
    let top, left;
    
    switch (position) {
      case 'top':
        top = triggerRect.top - tooltipRect.height - 8;
        left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
        break;
      case 'bottom':
        top = triggerRect.bottom + 8;
        left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
        break;
      case 'left':
        top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
        left = triggerRect.left - tooltipRect.width - 8;
        break;
      case 'right':
        top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
        left = triggerRect.right + 8;
        break;
    }
    
    // 确保提示不超出视窗
    const margin = 8;
    top = Math.max(margin, Math.min(window.innerHeight - tooltipRect.height - margin, top));
    left = Math.max(margin, Math.min(window.innerWidth - tooltipRect.width - margin, left));
    
    tooltip.style.top = top + window.scrollY + 'px';
    tooltip.style.left = left + window.scrollX + 'px';
  }
}

// 初始化工具提示管理器
const tooltipManager = new TooltipManager();

简单工具提示

javascript
function initTooltips() {
  const triggers = document.querySelectorAll('[data-tooltip]');
  
  triggers.forEach(trigger => {
    let tooltip = null;
    let timeoutId = null;
    
    function showTooltip() {
      if (tooltip) return;
      
      const content = trigger.dataset.tooltip;
      const position = trigger.dataset.tooltipPosition || 'top';
      const delay = parseInt(trigger.dataset.tooltipDelay) || 0;
      
      timeoutId = setTimeout(() => {
        tooltip = createTooltip(content, position);
        document.body.appendChild(tooltip);
        positionTooltip(trigger, tooltip, position);
        
        setTimeout(() => {
          tooltip.classList.add('show');
        }, 10);
      }, delay);
    }
    
    function hideTooltip() {
      if (timeoutId) {
        clearTimeout(timeoutId);
        timeoutId = null;
      }
      
      if (tooltip) {
        tooltip.classList.remove('show');
        setTimeout(() => {
          if (tooltip && tooltip.parentNode) {
            tooltip.parentNode.removeChild(tooltip);
          }
          tooltip = null;
        }, 200);
      }
    }
    
    trigger.addEventListener('mouseenter', showTooltip);
    trigger.addEventListener('mouseleave', hideTooltip);
  });
}

function createTooltip(content, position) {
  const tooltip = document.createElement('div');
  tooltip.className = `glass-tooltip ${position}`;
  tooltip.textContent = content;
  
  const arrow = document.createElement('div');
  arrow.className = 'tooltip-arrow';
  tooltip.appendChild(arrow);
  
  return tooltip;
}

function positionTooltip(trigger, tooltip, position) {
  const triggerRect = trigger.getBoundingClientRect();
  const tooltipRect = tooltip.getBoundingClientRect();
  
  let top, left;
  
  switch (position) {
    case 'top':
      top = triggerRect.top - tooltipRect.height - 8;
      left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
      break;
    case 'bottom':
      top = triggerRect.bottom + 8;
      left = triggerRect.left + (triggerRect.width - tooltipRect.width) / 2;
      break;
    case 'left':
      top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
      left = triggerRect.left - tooltipRect.width - 8;
      break;
    case 'right':
      top = triggerRect.top + (triggerRect.height - tooltipRect.height) / 2;
      left = triggerRect.right + 8;
      break;
  }
  
  tooltip.style.top = top + window.scrollY + 'px';
  tooltip.style.left = left + window.scrollX + 'px';
}

// 页面加载完成后初始化
document.addEventListener('DOMContentLoaded', initTooltips);

应用场景

按钮说明

html
<div class="button-group">
  <button class="glass-button" data-tooltip="保存当前文档">
    💾 保存
  </button>
  <button class="glass-button" data-tooltip="撤销上一步操作">
    ↩️ 撤销
  </button>
  <button class="glass-button" data-tooltip="重做上一步操作">
    ↪️ 重做
  </button>
  <button class="glass-button danger" data-tooltip="永久删除选中项目">
    🗑️ 删除
  </button>
</div>

表单字段帮助

html
<form class="glass-form">
  <div class="form-group">
    <label class="form-label">
      用户名
      <span class="help-icon" data-tooltip="用户名长度为3-20个字符,只能包含字母和数字">❓</span>
    </label>
    <input type="text" class="glass-input" placeholder="请输入用户名">
  </div>
  
  <div class="form-group">
    <label class="form-label">
      密码强度
      <span class="help-icon" data-tooltip="密码应包含大小写字母、数字和特殊字符,长度至少8位">❓</span>
    </label>
    <input type="password" class="glass-input" placeholder="请输入密码">
  </div>
  
  <div class="form-group">
    <label class="form-label">
      邮箱验证
      <span class="help-icon" data-tooltip="用于接收重要通知和密码重置邮件">❓</span>
    </label>
    <input type="email" class="glass-input" placeholder="请输入邮箱地址">
  </div>
</form>

图标功能说明

html
<div class="toolbar">
  <button class="tool-button" data-tooltip="粗体文字" data-tooltip-position="bottom">
    <strong>B</strong>
  </button>
  <button class="tool-button" data-tooltip="斜体文字" data-tooltip-position="bottom">
    <em>I</em>
  </button>
  <button class="tool-button" data-tooltip="下划线" data-tooltip-position="bottom">
    <u>U</u>
  </button>
  <button class="tool-button" data-tooltip="插入链接" data-tooltip-position="bottom">
    🔗
  </button>
  <button class="tool-button" data-tooltip="插入图片" data-tooltip-position="bottom">
    🖼️
  </button>
</div>

状态指示器

html
<div class="status-panel">
  <div class="status-item">
    <span class="status-dot success" data-tooltip="服务运行正常"></span>
    <span>API 服务</span>
  </div>
  <div class="status-item">
    <span class="status-dot warning" data-tooltip="响应时间较慢"></span>
    <span>数据库</span>
  </div>
  <div class="status-item">
    <span class="status-dot error" data-tooltip="连接失败"></span>
    <span>缓存服务</span>
  </div>
</div>

<style>
.status-dot {
  display: inline-block;
  width: 8px;
  height: 8px;
  border-radius: 50%;
  margin-right: 8px;
}

.status-dot.success { background: #4CAF50; }
.status-dot.warning { background: #FF9800; }
.status-dot.error { background: #F44336; }
</style>

Released under the MIT License.