网页菜单纯 css 实现

前言

  最近搞了些前端的工作,本来做菜单栏的时候想直接用 bootstrap 的,但是感觉 bootstrap 太大了,而且依赖有点多,在 webpack 中也不是很好打包(虽然可以绕过去),所以就索性自己在网上找了一些实现方式,改改感觉也还可以。这次主要实现了两种菜单栏,具体如下。

鼠标悬停下拉菜单

  鼠标悬停下拉菜单应该是最常见的一种菜单栏了,当鼠标悬停在菜单栏上时,子菜单缓缓下拉,看起来就很舒服,用 flex 布局结合列表也很好实现(不用像以前那种 float 了,舒服)。具体实现方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
<style>
.flex-body {
display: flex;
flex-direction: column;
}

header {
z-index: 1;
}

ul,
ul li {
list-style: none;
margin: 0;
padding: 0;
}

.menu {
display: flex;
justify-content: start;
}

ul li {
width: 100px;
height: 50px;
line-height: 50px;
}

.menu li .submenu {
/* display: none; */
background-color: aqua;
}

.submenu li {
height: 0;
line-height: 0;
opacity: 0;
visibility: hidden;
}

.menu li:hover .submenu li {
/* display: block; */
height: 50px;
line-height: 50px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="flex-body">
<header>
<ul class="menu">
<li>
<a>menu1</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu2</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
<li>
<a>menu3</a>
<ul class="submenu">
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
<li><a>submenu1</a></li>
</ul>
</li>
</ul>
</header>
<main>mainmainmainmainmainmainmainmainmainmain</main>
</div>

  ※注: 实现该菜单栏有两个需要注意的点:1、不能用 display: noneblock 来使子菜单消失或出现,因为这样会造成缓缓下拉的动画失效,transition 并不支持 display,所以只能用 visibilityheight 来共同实现,以达到下拉动画效果;2、因为使用了 flex 布局,所以 z-index 只对同级 flex-item 有效,所以为防止菜单栏下面的内容出现在子菜单之上,即将子菜单栏位于最上层,需要将整个页面的布局都设置为 flex,并使 headerz-index 最大,如此才能保证子菜单的菜单覆盖 main 中的内容,不然就会有重叠干扰现象。

鼠标点击手风琴菜单

  手风琴特效也算是非常常见的了,一般的手风琴是鼠标悬停展开,这种比较好实现,难的是如何保持这种展开状态,focus 可以短暂保持展开状态,但是不能点击其他地方,局限性太大。所以需要引入其它的东西来记录这种展开状态,可以用 checkboxradiochecked 来记录这种状态,从而只用 css 即可实现该菜单,具体实现如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
<style>
input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 500px;
height: 50px;
line-height: 50px;
border: 1px solid black;
}

.menu-content {
width: 500px;
max-height: 100px;
overflow-y: auto;
}

.menu-content>li {
height: 0;
line-height: 20px;
overflow: auto;
opacity: 0;
visibility: hidden;
transition: all 1s;
}

input[data-prop="menu-recorder"]:checked+.menu-content>li {
height: 20px;
line-height: 20px;
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="accordion-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="checkbox" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  这里需要注意一点的就是 height 从 0 到 100% 并不会触发 transition 渐变动画,而是需要确切的高度值变化才能触发,所以上文这里添加了个 <li> 标签,直接在该标签上添加动画,还有一种就是在 .menu-content 上设定确定的 max-height,也能触发动画,但是有个缺点就是前后 max-height 的差距太大时,动画效果就很不理想了,这时可能只能依靠 js 了。上文中 checkbox 也可用 radio 替换,效果略有差异,一个是能全部展开或收起,而另一个则是能且仅能展开一个。

Tab 标签页切换菜单

  这个菜单和上面那个菜单的实现非常相似,先上代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
<style>
.tab-menu {
display: flex;
position: relative;
width: 500px;
height: 300px;
}

input[data-prop="menu-recorder"] {
display: none;
}

.menu-title {
display: block;
width: 100px;
line-height: 50px;
text-align: center;
border: 1px solid black;
border-right: 0;
box-sizing: border-box;
transition: all 1s;
}

.tab-menu .menu-item:last-child .menu-title {
border-right: 1px solid black;
}

.menu-content {
position: absolute;
left: 0;
top: 51px;
height: calc(100% - 50px);
overflow-y: auto;
width: 100%;
border: 1px solid #000;
box-sizing: border-box;
font-size: 24px;
text-align: center;
opacity: 0;
visibility: hidden;
transition: all 1s;

}

input[data-prop="menu-recorder"]:checked+.menu-content {
opacity: 1;
visibility: visible;
transition: all 1s;
}
</style>
<div class="tab-menu">
<section class="menu-item">
<label class="menu-title" for="menu1">menu1</label>
<input id="menu1" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu2">menu2</label>
<input id="menu2" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu3">menu3</label>
<input id="menu3" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
<section class="menu-item">
<label class="menu-title" for="menu4">menu4</label>
<input id="menu4" type="radio" name="tab-control" data-prop="menu-recorder">
<div class="menu-content">
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
<li>test<br></li>
</div>
</section>
</div>

  从代码上看关键点就是 .menu-content 的定位方式了,采用了绝对定位,并将整个菜单栏设置为相对定位,以保证所有 tab 标签页内容位置和大小保持一致。当然 tab 标签页肯定是唯一的,所以只能用 radio 记录显示标签页了。其中为了保证 .menu-title.menu-content 的边框不重叠,所以在 .menu-title 中只设置 line-height,而 .menu-contenttop 比其多一个像素。

后记

  这三种菜单应该是最常见也是用的最多的了,纯 css 实现的方式也比较类似,无非就是 flex 布局以及借助 css3 强大的选择器功能(父类选择器不知要到猴年马月了,比较遗憾 😥),就能相对简单的实现了,当然借助一些 js 库或框架可能会更简单一些 ,但能用 css 为何不用呢 😄。

参考资料

[1] 利用flex实现的二级导航栏

[2] CSS3动画下拉菜单(当transition遇到display的坑)

[3] CSS3手风琴下拉菜单

[4] 教你两招用纯CSS写Tab切换