下拉菜单HTML/CSS/JS实现

第一部分:下拉菜单HTML/CSS基础实现

1.1 核心设计思路

老师强调:下拉菜单的本质是"容器+两块区域"------一块是对外展示的可见区域(如"学位管理"),另一块是默认隐藏的下拉内容区(二级菜单),鼠标悬停时展示,离开时隐藏。

HTML结构:

复制代码
<ul class="menu">
    <li>                          <!-- 一级菜单项 -->
        <a href="#">学位管理</a>   <!-- 对外展示区域 -->
        <ul class="model">        <!-- 下拉隐藏区域 -->
            <li><a href="#">学位委员会</a></li>
            <li><a href="#">政策法规</a></li>
        </ul>
    </li>
    <!-- 更多菜单项 -->
</ul>

层级关系:

复制代码
ul.menu(最外层容器)
    └── li(一级菜单项,水平排列)
            ├── a(对外展示的标签)
            └── ul.model(二级菜单,默认隐藏)
                    └── li(二级菜单项,垂直排列)
                            └── a

1.2 CSS 基础样式

复制代码
/* 重置默认样式 */
* { padding: 0; margin: 0; }
li { list-style: none; }
a { text-decoration: none; color: #333; }

/* 菜单容器:弹性盒子实现水平排列 */
.menu {
    width: 800px;
    background: rgb(235, 235, 235);
    margin: 180px auto;
    display: flex;
    justify-content: space-between;
}

/* ⭐ 一级li:子选择器 > 只选中直接子级 */
.menu > li {
    width: 180px;           /* 每个菜单项宽度 */
    text-align: center;
    position: relative;     /* 为二级菜单提供定位参照 */
}

/* 二级菜单:绝对定位脱离文档流 */
.model {
    position: absolute;
    background: rgb(235, 235, 235);
    width: 100%;            /* 相对于父级li的宽度 */
    height: 0;              /* 初始高度0,配合过渡 */
    overflow: hidden;       /* 隐藏超出部分 */
    transition: all 0.5s ease;  /* 平滑过渡 */
}

老师强调 :用>子选择器(如.menu > li)只选中一级li,避免误选二级li。直接写li会命中所有层级的li,容易造成样式错乱。

1.3 关键知识点:定位与脱离文档流

老师强调 :灰色区域的高度默认会被内部最高的子元素撑起来。若不想让二级ul撑起父容器高度,需给它设position: absolute使其脱离文档流,空间不再保留,父容器便不被撑高。

定位参照系:

定位方式 参照物 说明
absolute(父级无relative 浏览器视口 相对整个页面定位
absolute(父级有relative 父级元素 相对最近的定位父元素
relative(父级) 自身原始位置 为子元素提供定位参照
复制代码
/* 父级设relative,子级absolute相对父级定位 */
.menu > li {
    position: relative;   /* 作为子元素参照 */
}
.model {
    position: absolute;   /* 相对父级li定位 */
    width: 100%;          /* 占满父级宽度 */
}

老师强调 :脱离默认布局不等于切断父子关系,百分比参照系始终是父级。子元素用%仍是相对于父级计算。

1.4 内联元素 vs 块级元素

老师强调 :给a标签加padding撑不开外层容器,因为a标签是内联级元素 ,就像沙子堆在石头旁,没法把石头的边界撑开。要撑起父级,得把a改成块级元素

复制代码
/* 解决:将a标签设为块级元素 */
.menu a {
    display: block;       /* 块级化,能撑开父容器 */
    padding: 5px;
}

内联 vs 块级:

特性 内联元素(inline 块级元素(block
是否独占一行
宽高是否生效
padding是否撑开父级

第二部分:JS交互实现(鼠标悬停版)

2.1 完整代码

复制代码
window.onload = function() {
    // 获取所有一级菜单项
    var list = document.querySelectorAll(".menu > li");
    
    for (var i = 0; i < list.length; i++) {
        // 鼠标移入:展开
        list[i].onmouseenter = function() {
            var model = this.querySelector(".model");
            if (model) {
                // 计算二级菜单总高度
                var liList = model.querySelectorAll("li");
                var height = liList[0].clientHeight * liList.length;
                model.style.height = height + "px";
            }
        };
        
        // 鼠标移出:收起
        list[i].onmouseleave = function() {
            var model = this.querySelector(".model");
            if (model) {
                model.style.height = "0";
            }
        };
    }
};

2.2 执行顺序与元素加载

老师强调 :用选择器找元素时,常因JS执行时元素尚未生成而获取不到。解决办法:加defer延迟加载,或把逻辑绑在window.onload上。

复制代码
<!-- 方案1:defer延迟加载 -->
<script src="js/下拉框.js" defer></script>

<!-- 方案2:window.onload -->
<script>
window.onload = function() {
    // 确保元素已加载
};
</script>

2.3 动态高度计算

老师强调 :固定高度写死(如200px)会导致内容溢出。正确做法是动态计算:获取二级菜单下所有li,用单个li高度乘以li数量,得到总高度。

复制代码
var model = this.querySelector(".model");
var liList = model.querySelectorAll("li");
// clientHeight = 内容区 + 填充区(不含边框)
var height = liList[0].clientHeight * liList.length;
model.style.height = height + "px";

clientHeight 说明:

复制代码
┌─────────────────────┐
│  ┌───────────────┐  │
│  │   content     │  │  ← 内容区
│  ├───────────────┤  │
│  │   padding     │  │  ← 填充区
│  └───────────────┘  │
└─────────────────────┘
clientHeight = content + padding(不含边框)

2.4 if 判断中的真假值

复制代码
// 用if判断元素是否存在,避免报错
if (this.querySelector(".model")) {
    // 存在才执行
    var model = this.querySelector(".model");
    // ...
}

JS真假值规则:

类型 false 情况 true 情况
数字 0NaN 其他数值
布尔 false true
字符串 ""(空字符串) 非空字符串
对象 null 任何非空对象

第三部分:CSS过渡动画(Transition)

3.1 什么是过渡动画

老师强调 :元素状态变化默认是"嗖"一下瞬间完成。transition把瞬间变化变成平滑渐变过程------就像灯一下亮和调光器渐亮的区别。

复制代码
.test {
    width: 200px;
    height: 100px;
    background: skyblue;
    transition: all 3s ease;   /* 所有属性3秒渐变 */
}

.test:hover {
    width: 500px;
    height: 300px;
    background: red;
}

transition 子属性:

属性 说明 示例值
transition-property 指定要过渡的属性 widthall
transition-duration 过渡持续时间 3s0.5s
transition-timing-function 速度曲线 easelinear

timing-function 常用值:

效果
ease 慢→快→慢(默认)
linear 匀速
ease-in 先慢后快
ease-out 先快后慢

3.2 ⭐ display 不支持过渡

老师强调display: nonedisplay: block 不支持过渡动画,显隐都是"嗖"一下。想平滑展开/收起,不能用display,要用高度(height)配合overflow: hidden

复制代码
/* ❌ display不支持过渡 */
.model {
    display: none;
    transition: all 0.5s ease;   /* 无效! */
}
.model:hover {
    display: block;
}

/* ✅ 高度支持过渡 */
.model {
    height: 0;
    overflow: hidden;
    transition: all 0.5s ease;   /* 有效! */
}
.model:hover {
    height: 200px;   /* 配合JS动态计算更好 */
}

支持过渡 vs 不支持过渡:

属性 是否支持过渡
widthheight
colorbackground
opacity
display
position

第四部分:点击交互与事件传播

4.1 点击版本完整代码

复制代码
window.onload = function() {
    var list = document.querySelectorAll(".menu > li");
    var modelList = document.querySelectorAll(".model");

    for (var i = 0; i < list.length; i++) {
        list[i].onclick = function(e) {
            // ⭐ 阻止事件传播(关键!)
            e.stopPropagation();

            // 先全部隐藏
            for (var j = 0; j < modelList.length; j++) {
                modelList[j].style.height = 0;
            }

            // 再显示当前项
            var model = this.querySelector(".model");
            if (model) {
                var liList = model.querySelectorAll("li");
                var height = liList[0].clientHeight * liList.length;
                model.style.height = height + "px";
            }
        };
    }

    // 点击空白区域全部收起
    document.onclick = function() {
        for (var j = 0; j < modelList.length; j++) {
            modelList[j].style.height = 0;
        }
    };
};

核心逻辑:

复制代码
点击菜单项
    │
    ▼
1. 阻止事件传播(stopPropagation)
    │
    ▼
2. 全部隐藏(所有model高度设为0)
    │
    ▼
3. 显示当前项(只有被点击的model展开)

4.2 事件传播机制

老师强调 :点击嵌套结构中的子元素时,不仅触发自身click,还会因事件冒泡 触发父级click。就像涟漪效应------点外层会波及内层。

复制代码
点击子元素时的事件传播路径:
document → ... → 父级li → 子元素
   ↑
事件冒泡方向(从内到外)

事件传播的两种行为:

阶段 方向 说明
捕获 从外到内 document到目标元素
冒泡 从内到外 从目标元素到document(默认)

4.3 阻止事件传播:stopPropagation()

老师强调 :用onclick无法阻止事件传播。改用addEventListener配合e.stopPropagation(),可阻止事件继续冒泡到父级。

复制代码
// ❌ onclick无法阻止传播
list[i].onclick = function(e) {
    // 无法阻止父级触发
};

// ✅ addEventListener + stopPropagation
list[i].onclick = function(e) {
    e.stopPropagation();   // 阻止事件向上传播
    // 只触发当前元素的逻辑
};

阻止传播的效果:

复制代码
点击前:点击子元素 → 子元素click触发 → 父元素click也触发
点击后:点击子元素 → 子元素click触发 → 父元素click不触发

4.4 点击空白收回

复制代码
// 点击document空白区域,全部收起
document.onclick = function() {
    for (var j = 0; j < modelList.length; j++) {
        modelList[j].style.height = 0;
    }
};

老师强调:这个逻辑是"先关所有灯,再只开当前房间那盏"。用"先全部隐藏再定向显示"的思路,比记录之前谁显示更简洁,避免状态管理混乱。

第五部分:鼠标悬停版 vs 点击版对比

对比项 悬停版(hover) 点击版(click)
触发方式 鼠标移入/移出 鼠标点击
收起方式 自动(移出即收) 手动(点击其他区域)
事件传播问题 stopPropagation()
适用场景 传统导航菜单 移动端/下拉选择器
代码复杂度 较简单 较复杂

附录一:完整代码汇总

HTML(下拉框.html)

复制代码
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>下拉菜单</title>
    <link rel="stylesheet" href="css/下拉框.css">
    <script src="js/下拉框.js" defer></script>
</head>
<body>
    <ul class="menu">
        <li>
            <a href="#">学位管理</a>
            <ul class="model">
                <li><a href="#">学位委员会</a></li>
                <li><a href="#">政策法规</a></li>
            </ul>
        </li>
        <li>
            <a href="#">导师管理</a>
            <ul class="model">
                <li><a href="#">学位委员会</a></li>
                <li><a href="#">政策法规</a></li>
                <li><a href="#">常置信息</a></li>
                <li><a href="#">优博优硕</a></li>
            </ul>
        </li>
        <li>
            <a href="#">学生工作</a>
            <ul class="model">
                <li><a href="#">学位委员会</a></li>
                <li><a href="#">政策法规</a></li>
                <li><a href="#">常置信息</a></li>
                <li><a href="#">政策法规</a></li>
                <li><a href="#">常置信息</a></li>
                <li><a href="#">优博优硕</a></li>
            </ul>
        </li>
        <li>
            <a href="#">下载专区</a>
            <ul class="model">
                <li><a href="#">学位委员会</a></li>
                <li><a href="#">政策法规</a></li>
                <li><a href="#">常置信息</a></li>
                <li><a href="#">优博优硕</a></li>
            </ul>
        </li>
    </ul>
</body>
</html>

CSS(下拉框.css)

复制代码
* { padding: 0; margin: 0; }
li { list-style: none; }

a {
    text-decoration: none;
    color: #333;
    padding: 5px;
    display: block;
}

a:hover {
    background: #333;
    opacity: 0.6;
    color: #fff;
}

.menu {
    width: 800px;
    background: rgb(235, 235, 235);
    margin: 180px auto;
    display: flex;
    justify-content: space-between;
}

.menu > li {
    width: 180px;
    text-align: center;
    position: relative;
}

.model {
    position: absolute;
    background: rgb(235, 235, 235);
    width: 100%;
    height: 0;
    overflow: hidden;
    transition: all 0.5s ease;
}

JavaScript(点击版 - 下拉框.js)

复制代码
window.onload = function() {
    var list = document.querySelectorAll(".menu > li");
    var modelList = document.querySelectorAll(".model");

    for (var i = 0; i < list.length; i++) {
        list[i].onclick = function(e) {
            e.stopPropagation();

            for (var j = 0; j < modelList.length; j++) {
                modelList[j].style.height = 0;
            }

            var model = this.querySelector(".model");
            if (model) {
                var liList = model.querySelectorAll("li");
                var height = liList[0].clientHeight * liList.length;
                model.style.height = height + "px";
            }
        };
    }

    document.onclick = function() {
        for (var j = 0; j < modelList.length; j++) {
            modelList[j].style.height = 0;
        }
    };
};
相关推荐
2601_9620715722 分钟前
Java进阶(vue基础)
前端·javascript·vue.js
研☆香22 分钟前
数组方法 splice讲解 拓展
开发语言·前端·javascript
码视野24 分钟前
基于 Spring Boot + Vue3 的【城市地下燃气管网微泄漏感知与相邻地下空间燃爆预警中台】设计与实现(含PRD/三端高保真源码/大屏)
java·前端·人工智能·spring boot·后端
淡海水26 分钟前
05-03-栈队列-PriorityQueue-TElement-TPriority-NET6优先队列语义与四叉堆实现
服务器·前端·c#·priorityqueue·clr·telement
ITresearchGuest1 小时前
LangChain JS 入门:快速搭建前端 AI 开发环境
前端·javascript·langchain
YHHLAI1 小时前
React + TypeScript 实战:从零构建颜色选择器应用
前端·react.js·typescript
Profile排查笔记1 小时前
指纹浏览器手机版怎么选?从本地 App 到云端 Android 的实现方式解析
前端·人工智能·后端·自动化
浪兎兎2 小时前
Vue CLI笔记
前端·vue.js·笔记
Csvn2 小时前
Proxy / Reflect 与响应式原理:完整手写一个 mini Vue3 响应式系统
前端