纯 HTML/CSS/JS 实现下拉菜单组件
功能概述
本下拉菜单组件实现以下核心功能:
- 点击/悬停触发显示
- 自动定位与边界检测
- 平滑过渡动画
- 点击菜单项自动关闭
- 外部点击/ESC 键关闭
- 响应式布局
实现原理
1. 事件处理机制
- 触发逻辑:通过
click或mouseenter事件监听触发元素 - 冒泡控制:使用
e.stopPropagation()阻止菜单内部事件传播 - 全局监听:在 document 上注册点击事件实现外部关闭检测
- 键盘交互:监听 ESC 键关闭菜单
2. 定位系统
- 初始定位:通过
getBoundingClientRect()获取触发元素位置 - 滚动补偿:结合
pageXOffset/pageYOffset计算滚动偏移 - 边界检测:动态检测视口边缘并自动调整位置
3. 动画系统
- CSS 过渡动画:使用
transition实现透明度与位移动画 - 类名控制:通过
active类名切换显示状态
代码解析
HTML 结构
<button class="dropdown-trigger">显示菜单</button>
<div class="dropdown-overlay">
<ul class="dropdown-menu">
<li>新建文件</li>
<li>打开文件</li>
<li>保存内容</li>
<li>退出系统</li>
</ul>
</div>
CSS 样式
body {
padding: 50px;
font-family: Arial, sans-serif;
}
.dropdown-trigger {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
position: relative;
}
.dropdown-overlay {
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
z-index: 1000;
min-width: 150px;
}
.dropdown-overlay.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-menu {
list-style: none;
margin: 0;
padding: 8px 0;
}
.dropdown-menu li {
padding: 8px 16px;
cursor: pointer;
transition: background 0.2s;
}
.dropdown-menu li:hover {
background: #f5f5f5;
}
JavaScript 逻辑
// 定位计算
function positionDropdown() {
const triggerRect = trigger.getBoundingClientRect();
overlay.style.left = `${triggerRect.left + window.pageXOffset}px`;
overlay.style.top = `${triggerRect.bottom + window.pageYOffset + 4}px`;
}
// 边界检测
function checkBoundary() {
if (overlayRect.bottom > viewportHeight) {
overlay.style.top = `${triggerRect.top - overlayRect.height - 4}px`;
}
}
// 事件监听
menuItems.forEach(item => {
item.addEventListener('click', () => toggleDropdown(false));
});
## 完整实现代码
```html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Dropdown with Item Click</title>
<style>
body {
padding: 50px;
font-family: Arial, sans-serif;
}
.dropdown-trigger {
padding: 8px 16px;
background: #007bff;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
position: relative;
}
.dropdown-overlay {
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 8px rgba(0,0,0,0.15);
opacity: 0;
transform: translateY(-10px);
transition: opacity 0.2s, transform 0.2s;
pointer-events: none;
z-index: 1000;
min-width: 150px;
}
.dropdown-overlay.active {
opacity: 1;
transform: translateY(0);
pointer-events: auto;
}
.dropdown-menu {
list-style: none;
margin: 0;
padding: 8px 0;
}
.dropdown-menu li {
padding: 8px 16px;
cursor: pointer;
transition: background 0.2s;
}
.dropdown-menu li:hover {
background: #f5f5f5;
}
</style>
</head>
<body>
<button class="dropdown-trigger">显示菜单</button>
<div class="dropdown-overlay">
<ul class="dropdown-menu">
<li>新建文件</li>
<li>打开文件</li>
<li>保存内容</li>
<li>退出系统</li>
</ul>
</div>
<script>
const trigger = document.querySelector('.dropdown-trigger');
const overlay = document.querySelector('.dropdown-overlay');
const menuItems = document.querySelectorAll('.dropdown-menu li');
// 定位函数
function positionDropdown() {
const triggerRect = trigger.getBoundingClientRect();
overlay.style.left = `${triggerRect.left + window.pageXOffset}px`;
overlay.style.top = `${triggerRect.bottom + window.pageYOffset + 4}px`;
}
// 切换显示状态
function toggleDropdown(show) {
overlay.classList.toggle('active', show);
if (show) {
positionDropdown();
checkBoundary();
}
}
// 新增:菜单项点击处理
function handleMenuItemClick(e) {
toggleDropdown(false);
console.log('选择了:', e.target.textContent);
// 这里可以添加具体的菜单项处理逻辑
}
// 为所有菜单项绑定点击事件
menuItems.forEach(item => {
item.addEventListener('click', handleMenuItemClick);
});
// 其他原有代码保持不变...
// (保持之前的边界检测、事件监听等代码)
// 边界检测函数
function checkBoundary() {
const overlayRect = overlay.getBoundingClientRect();
const viewportWidth = window.innerWidth;
const viewportHeight = window.innerHeight;
if (overlayRect.right > viewportWidth) {
overlay.style.left = `${viewportWidth - overlayRect.width - 8}px`;
}
if (overlayRect.bottom > viewportHeight) {
overlay.style.top = `${trigger.getBoundingClientRect().top + window.pageYOffset - overlayRect.height - 4}px`;
}
}
trigger.addEventListener('click', (e) => {
e.stopPropagation();
toggleDropdown(!overlay.classList.contains('active'));
});
document.addEventListener('click', (e) => {
if (!overlay.contains(e.target) && !trigger.contains(e.target)) {
toggleDropdown(false);
}
});
// trigger.addEventListener('mouseenter', () => toggleDropdown(true));
// overlay.addEventListener('mouseleave', () => toggleDropdown(false));
document.addEventListener('keydown', (e) => {
if (e.key === 'Escape') {
toggleDropdown(false);
}
});
window.addEventListener('resize', () => {
if (overlay.classList.contains('active')) {
positionDropdown();
checkBoundary();
}
});
</script>
</body>
</html>
扩展建议
- 动画优化
.dropdown-overlay {
transition: opacity 0.2s cubic-bezier(0.4, 0, 0.2, 1),
transform 0.2s cubic-bezier(0.4, 0, 0.2, 1);
}
- 无障碍支持
<button
aria-haspopup="true"
aria-expanded="false"
aria-label="操作菜单"
>
显示菜单
</button>
- 多级菜单
function initSubmenu() {
document.querySelectorAll('.submenu').forEach(menu => {
menu.previousElementSibling.addEventListener('click', (e) => {
e.stopPropagation();
menu.classList.toggle('show');
});
});
}
注意事项
- 层级问题:确保菜单的
z-index高于页面其他元素 - 移动端适配:添加
touchstart事件支持 - 性能优化:使用防抖技术处理 resize 事件
- 服务端渲染:动态特性需在
DOMContentLoaded后初始化
本实现方案通过纯前端技术实现了企业级下拉菜单的核心功能,可根据具体需求进行功能扩展和样式定制。