6.1 导航菜单
菜单由一组链接组成。用HTML中的列表元素(ul
或ol
)来分组链接不仅符合逻辑,而且即使没有额外的CSS也能适当显示链接的层次。默认情况下,由于列表项(li
)是块级元素,因此它们会上下堆叠。
6.1.1 纵向菜单
先从最简单的情况开始讲,通过下面这个简单的例子,你会知道把列表转换成菜单的关键技术。
<nav class="list1">
<ul>
<li><a href="#">Alternative</a></li>
<li><a href="#">Country</a></li>
<li><a href="#">Jazz</a></li>
<li><a href="#">Rock</a></li>
</ul>
</nav>
以上标记给每个列表项的文本都加了链接标签,让它们都变得可以点击。(每个链接中的#
是URL占位符,将来可以替换成真正的URL。)
首先,我们通过第一个例子来展示几个问题,很多初学者在为导航链接写样式的时候都会遇到这些问题。然后,再告诉你怎么解决它们。无论如何,这些CSS应用到HTML之后,会得到一个看起来很简洁的菜单,如图6-1所示。
- /*去掉默认的内边距和外边距*/
- * {margin:0; padding:0;}
- /*设定本例中菜单的大小和位置*/
- nav {margin:50px; width:150px;}
- /*给菜单加上边框*/
- .list1 ul {border:1px solid #f00; border-radius:3px;
- padding:5px 10px 3px;}
- /*去掉项目符号并为链接添加间距*/
- .list1 li {list-style-type:none; padding:3px 10px;}
- /*“非首位子元素”选择符*/
- .list1 li + li {border-top:1px solid #f00;}
- /*为链接添加样式*/
- .list1 a {text-decoration:none; font:20px Exo, helvetica,
- arial, sans-serif; font-weight:400; color:#000;
- background:#ffed53;}
- /*悬停高亮*/
- .list1 a:hover {color:#069;}
图6-1 应用样式之后的无序列表,从箭头光标可知,没有文本的区域是不能点击的
添加这些样式没什么难度。HTML5新增的nav
元素在语义上很恰当,因此我们用它作为导航菜单的容器,还给它添加了一个类作为设定样式的上下文。
使用“非首位子元素”选择符
前面的CSS代码中有两点特别值得一说。首先,加粗的li + li
选择符的意思是“任何跟在li
之后的li
”。
对于连续的元素——比如这里多个li
,这个选择符会选择除第一个之外的所有li
元素。这样,就可以给除第一个li
之外的所有列表项上方加一条边框。假如我只简单使用li
选择符,那第一个列表项上方也将被加上边框,而这不是我们想要的。我把这种选择符称为“非首位子元素”选择符,它在选择列表时非常实用。当然,也有实现相同效果的其他方法,比如:
/*给所有li上方添加一条边框*/
li {border-top:1px solid #f00;}
/*去掉第一个li上方的边框*/
li:first-child {border-top:none;}
让列表行可以点击
在图6-2中,我为每个链接都加上了背景色,以便显示相应链接可点击区域的大小。如图所示,目前只有文本是可以点的,因为链接(a
)是行内元素,它会收缩并包住其中的文本。然而,更好的用户体验是让列表项所在的整行都能点击。之所以要重点讲这个问题,是因为我发现很多站点都没有这么做。结果就是用户必须用鼠标点击文本,点击文本之外的地方没有反应。
此外,在每个li
元素的上方和下方,各有3像素的内边距,导致链接文本与列表项边框之间有了距离。正因为有了这段距离,当用户鼠标移动到链接间的间隙时,光标会从“悬停于链接之上”时的小手形状,变成常规的箭头形状。
图6-2 临时加的背景色让我们看清了a
元素实际可以点击的区域大小,如图所示,光标在经过两个链接的间隙时从小手变成了指针
要解决上述问题,首先得把内边距从li
元素转移到链接内部,然后让链接完全填满整个列表项。
- * {margin:0; padding:0;}
- nav {margin:50px; width:150px;}
- .list1 ul {border:1px solid #f00; border-radius:3px;
- padding:5px 10px 3px;}
- .list1 li {list-style-type:none; padding:3px 10px;}
- .list1 li + li a {border-top:1px solid #f00;}
- .list1 a {display:block; padding:3px 10px; textdecoration:
- none; font:20px Exo, helvetica, arial, sansserif;
- font-weight:400; color:#000; background:#ffed53;}
- .list1 a:hover {color:#069;}
把选择符li + li
变成li + li a
,就可以把上边框添加到列表项后面的列表项所包含的链接元素上(虽然有点绕,但你应该明白我的意思吧?)。而且,我还把内边距从列表项转移到了链接内部。这样,链接上下就互相接触了,中间没了间隙,鼠标经过时光标状态也不会变了。最后,还要把每个链接的display
属性设定为block
,这样链接元素的盒子就会由“收缩包围内容”变成“扩展填充父元素”。换句话说,链接的可点击区域将扩大到整个列表行。图6-3清晰地说明了经过这些改进后的效果。
图6-3 每个链接都填满了整个列表项,所以整个区域都可以点击了。左图的背景色显示了可点击区域,而右图则显示了去掉测试用的背景色之后的最终效果
6.1.2 横向菜单
默认情况下,列表项是垂直堆叠在一起的。不过,要把它们变成水平排列的横向菜单也十分容易。方法就是浮动列表项。比如下面这个简单的列表:
<nav class="list1">
<ul>
<li><a href="#">Shirts</a></li>
<li><a href="#">Pants</a></li>
<li><a href="#">Dresses</a></li>
<li><a href="#">Shoes</a></li>
<li><a href="#">Accessories</a></li>
</ul>
</nav>
和下面这些与前面创建纵向菜单很相似的CSS:
- .list1 ul {
- /*强制ul包围浮动的li元素*/
- overflow:hidden;
- }
- .list1 li {
- /*让li元素水平排列*/
- float:left;
- /*去掉项目符号*/
- list-style-type:none;
- }
- .list1 a {
- /*让链接填满li元素*/
- display:block;
- padding:0 16px;
- /*去掉链接的下划线*/
- text-decoration:none;
- color:#999;
- }
- .list1 li + li a {border-left:1px solid #aaa;}
- .list1 a:hover {color:#555;}
图6-4 简单的水平菜单
图6-4这个水平菜单是不是很常见?没错,很多时尚的零售网店都有它们的身影。请容我啰嗦几句,解释解释上面的CSS代码:浮动让li
元素从垂直变成水平,display:block
让链接从收缩变成扩张,从而整个li
元素都变成了可以点击的。另外,选择符li + li a
为除第一个链接之外的每个链接左侧都加了一条竖线,作为视觉分隔线。好啦,可以讲更复杂的样式了。
6.1.3 下拉菜单
在我收到的邮件中,询问下拉菜单问题的最多。所以,本节我就来给大家彻底讲清楚,看一看到底什么是下拉菜单,怎么创建下拉菜单,下拉菜单有什么变化形式。下拉菜单是以一组嵌套列表为基础,综合运用我们刚刚学到的纵向和横向菜单的CSS技术创建的。图6-5展示了一个下拉菜单。
图6-5 三级下拉菜单
为方便设定菜单样式,我为列表容器nav
添加了multi_drop_menu
类。这个菜单的每一条CSS规则都以.multi_drop_menu
开头,以确保它们只会应用给带有这个类的容器。
下拉菜单的标记是在前面菜单例子基础上扩充而来的。下面就是所有标记,虽然看起来挺吓人,但实际上不过是一个简单的三层嵌套列表而已。
- <nav class="multi_drop_menu">
- <!-- 一级开始 -->
- <ul>
- <li><a href="#">Power</a></li>
- <li><a href="#">Money</a></li>
- <li><a href="#">Love</a></li>
- <li><a href="#">Fame</a>
- <!-- 二级开始 -->
- <ul>
- <li><a href="#">Sports Star</a></li>
- <li><a href="#">Movie Star</a></li>
- <li><a href="#">Rock Star</a>
- <!-- 三级开始 -->
- <ul>
- <li><a href="#">Bruce Springsteen</a></li>
- <li><a href="#">Bono</a></li>
- <li><a href="#">Mick Jagger</a></li>
- <li><a href="#">Bob Dylan</a></li>
- </ul>
- <!-- 三级结束 -->
- </li>
- <li><a href="#">Web Designer</a></li>
- </ul>
- <!-- 二级结束 -->
- </li>
- </ul>
- <!-- 一级结束 -->
- </nav>
以上标记只包含图6-5中展示的菜单项。想嵌套更多子菜单也非常简单,方法是在标记中找到相应链接,把包含子菜单项的无序列表加在链接之后、包含它的li
元素结束标签之前。一会儿你就知道了,这样做无须更改CSS。下面就从创建水平的顶级菜单开始吧。
顶级菜单
以下是顶级菜单的CSS(请配合注释理解它们的作用):
/*添加视觉样式*/
.multi_drop_menu {font:1em helvetica, arial, sans-serif;}
.multi_drop_menu a {
/*让链接充满列表项*/
display:block;
/*文本颜色*/
color:#555;
/*背景颜色*/
background-color:#eee;
/*链接的内边距*/
padding:.2em 1em;
/*分隔线宽度*/
border-width:3px;
/*可以有颜色,也可以透明*/
border-color:transparent;
}
.multi_drop_menu a:hover {
/*悬停时文本颜色*/
color:#fff;
/*悬停时背景色*/
background-color:#aaa;
}
.multi_drop_menu a:active {
/*点击时背景变色*/
background:#fff;
/*点击时文本变色*/
color:#ccc;
}
/*添加功能样式*/
.multi_drop_menu * {margin:0; padding:0;}
/*强制ul包围li*/
.multi_drop_menu ul {float:left;}
.multi_drop_menu li {
/*水平排列菜单项*/
float:left;
/*去掉默认的项目符号*/
list-style-type:none;
/*为子菜单提供定位上下文*/
position:relative;
}
.multi_drop_menu li a {
/*让链接填充列表项*/
display:block;
/*给每个链接添加一个右边框*/
border-right-style:solid;
/*背景只出现在内边距区域后面*/
background-clip:padding-box;
/*去掉链接的下划线*/
text-decoration:none;
}
.multi_drop_menu li:last-child a {border-right-style:none;}
/*临时隐藏低级菜单*/
.multi_drop_menu li ul {display:none;}
图6-6 顶级菜单项,此刻鼠标正按住未释放,因此有:active
效果
对于图6-6的CSS样式,首先要注意的是,我把菜单的视觉样式与功能样式分开来写了。视觉样式控制字体大小、边框和文本的颜色,功能样式控制菜单的布局和行为。两者的区别通过代码注释是很容易分辨的。对于下拉菜单这么复杂的组件,分开来写视觉和功能代码是非常值得提倡的。这样,将来如果有人要修改菜单外观,只要修改它的视觉样式就好了,而功能样式可以原封不动。
如果你在代码中像这样把视觉样式与功能样式分开来写,那一定要用注释说明为什么。
目前,最明显的变化还是li
通过浮动由垂直堆叠变成了水平排列。至于为了让ul
包围列表项,没有使用overflow:hidden
,而是使用了float:left
,是因为前者会导致后来添加到下拉菜单中的子菜单无法显示——它们最终会显示在父元素ul
的外面,结果会因为“溢出”(overflow)而被隐藏(hidden)
为了保证用户体验,所有视觉样式——内边距、背景、边框,等等,都要应用给链接a
,而不要应用给ul
或li
,以便热区(可点击区域)最大化,让用户鼠标经过时不会产生前面例子中看到的状态切换。为达到这个目的,同时还要从视觉上分隔链接,我们使用了background-clip:padding-box
声明,这样就可以阻止链接的背景(像常规状态下一样)延伸到边框后面。然后,让边框透明(也可显示为其他实色),在链接之间产生间隙,让后面的页面能够透过边框被看到。如此一来,不用外边距也能分隔链接,而且鼠标从一个菜单项移动到另一个菜单项时,也不会出现光标切换。菜单项之间从视觉上是分开,但实际上却是紧挨在一块的。
要了解
background-clip
和透明边框的更多用法,可以阅读这篇文章:http://css-tricks.com/transparent-borders-with-background-clip。
或许你注意到了,我给li
元素应用了position:relative
,这是给添加子菜单作准备的,在当前这一步里实际上它没有什么效果。另外,最后一行CSS隐藏了子菜单,以防显示它们影响我们创建顶级菜单。在后面两步中,我会告诉你怎么在菜单项处于悬停状态时显示它们。
第一步为顶级菜单添加的样式都会被次级菜单继承。换句话说,需要应用给子菜单的样式大部分都已经写完啦!
菜单的下拉部分
虽然子菜单继承顶级菜单的样式为我们提供了方便,但其实有些样式在子菜单中并不是必要的。比如,(列表的第二级)下拉菜单是垂直堆叠的,所以我们不希望其中的列表项继承浮动而变成水平排列。另外,顶级菜单使用右边框作为分隔线,而下拉菜单需要使用上边框来分隔垂直堆叠的菜单项。这一步,我们还会绝对定位下拉菜单,以便它能精确地对齐父元素(包含子菜单的li
),而这个父元素已经在上一步被设定成相对定位了。
与此同时,为了看到这些设定的效果,还要显示二级下拉菜单,并继续隐藏三级下拉菜单(下一步再考虑它)。
以下是实现上述改变需要添加的CSS:
- /* 添加的视觉样式 */
- /*二级菜单宽度*/
- .multi_drop_menu li ul {width:9em;}
- .multi_drop_menu li li a {
- /*去掉继承的右边框*/
- border-right-style:none;
- /*添加上边框*/
- border-top-style:solid;
- }
- /* 添加的功能样式 */
- .multi_drop_menu li ul {
- /*临时显示二级下拉菜单*/
- display:block;
- /*相对于父菜单项定位*/
- position:absolute;
- /*左边与父菜单项对齐*/
- left:0;
- /*顶边与父菜单项底边对齐*/
- top:100%;
- }
- .multi_drop_menu li li {
- /*停止浮动,恢复堆叠*/
- float:none;
- }
- .multi_drop_menu li li ul {
- /*继续隐藏三级下拉菜单*/
- display:none;
- }
图6-7 二级下拉菜单已经显示在其父菜单项下方
这一步成功的关键是下拉菜单的绝对/相对定位。通过将其顶边位置(top
)设定为100%
(相对于其相对定位的父元素li
),其顶边会与父元素底边恰好对齐。它与父元素之间的间隙,实际上是下拉菜单中第一个链接的边框,参见图6-7。
让下拉菜单响应鼠标事件
接下来是好玩的部分了,即要让下拉菜单拥有自己的功能。换句话说,它一开始是隐藏的,只有在其父元素处于鼠标悬停状态时,才需要显示。
- .multi_drop_menu li ul {
- /*隐藏二级下拉菜单*/
- display:none;
- /*相对于父菜单项定位*/
- position:absolute;
- /*左边与父菜单项对齐*/
- left:0;
- /*顶边与父菜单项底边对齐*/
- top:100%;
- }
- .multi_drop_menu li:hover > ul {
- /*父元素悬停时显示*/
- display:block;
- }
图6-8 鼠标移动到链接上,其子菜单就会显示出来
让菜单起作用的关键在于先把它藏起来:
/*隐藏二级菜单*/
li ul {display:none;}
然后,再在父元素悬停时把它显示出来:
/*显示二级菜单*/
li:hover > ul {display:block;}
最后一行CSS的意思是:当鼠标移动到列表项上时,就显示它的子列表。注意,这里的:hover
触发器是设定在li
元素而非链接自身上的。这样做是因为我们想要显示li
的子元素ul
,而它不是想链接的子元素。此外,为了只显示其子元素,悬停列表项与子列表之间还有一个子选择符>
,结果如图6-8所示。如果没有这个子选择符,当顶级菜单项处于悬停状态时,会同时显示二级和三级菜单。
添加三级菜单
接下来我们就趁热打铁,把三级菜单也弄好吧。实际上,这时候三级菜单已经算是能用了。不信?找到图6-8的示例代码,在浏览器中打开,然后用鼠标触发子菜单,就会看到图6-9所示的效果。
图6-9 在父元素处于鼠标之下时,三级菜单也会显示,只不过位置不对
由于前面包含:hover
的CSS规则会像应用给二级菜单一样,应用给三级菜单,所以在父元素处于鼠标之下时,三级菜单自然也会显示出来。可是,图6-9中的三级菜单被其父元素挡住了。这时候三级菜单与其二级父元素的位置是什么关系?没错,就是二级菜单与顶级菜单的关系。所以,三级菜单跑到了鼠标下的二级菜单后面去了,而且其第一项顶边与悬停的父菜单项底边是对齐的。从图中可以看到,三级菜单第一项“Bruce Springsteen”的上半部分被二级菜单最后一项“Web Designer”给盖住了。
我们在这一步要做的,就是把三级菜单放到二级菜单右侧,让它的顶边与鼠标所在菜单项的底边对齐。
.multi_drop_menu li li ul {
/*相对于父菜单定位*/
position:absolute;
/*与父菜单右侧对齐*/
left:100%;
/*与父菜单项顶边对齐*/
top:0;
}
图6-10 三级下拉菜单显示在了二级菜单旁边
现在的菜单能用了,参见图6-10。你可以在顶级添加更多列表项,在标记中添加更多子列表,然后无须多写一行CSS,它们就能构成新的下拉菜单。此时,我还想再作两个调整。首先,为了真正让这些代码有用,而且可以重用,需要再写一些样式,让顶级菜单能够垂直显示,以便能将其用在导航侧边栏里。为此,我得先给nav
容器添加第二个类vertical
。
<!-- HTML类名之间要有空格 -->
<nav class="multi_drop_menu vertical">
这样,我就可以写一些样式,让它们只在导航容器有vertical
类的情况下才起作用。所以,这些新增规则的选择符开头都是
/*CSS类名之间没有空格*/
.multi_drop_menu.vertical
这意味着选择一个带有两个类的元素——注意,CSS的类名间没有空格。
- /*顶级垂直菜单宽度*/
- .multi_drop_menu.vertical {width:8em;}
- .multi_drop_menu.vertical li a {
- border-right-style:none;
- border-top-style:solid;
- }
- .multi_drop_menu.vertical li li a {border-left-style:solid;}
- .multi_drop_menu.vertical ul,
- .multi_drop_menu.vertical li {
- /*让顶级菜单垂直显示*/
- float:none;
- }
- .multi_drop_menu.vertical li ul {
- /*子菜单左边与上一级菜单右边对齐*/
- left:100%;
- /*子菜单顶边与上一级菜单项顶边对齐*/
- top:0;
- }
为了让菜单恢复默认的堆叠状态,这里重置了顶级li
元素及其父元素ul
的浮动属性,后者原来浮动是为了包围浮动的li
元素。
这里还为nav
容器设定了宽度。如果不设定导航容器宽度,那么nav
及其包含的顶级菜单项都会尽可能伸展。最后,我们把二级菜单与顶级菜单的位置关系,设定成了前面三级菜单和二级菜单之间的关系,即让子菜单顶边与父菜单项顶边对齐。当然,还去掉了顶级菜单项的右边框,代之以上边框,如图6-11所示。
图6-11 增加了垂直菜单样式后,顶级菜单项上下堆叠起来了
突出显示选择路径
图6-11显示,只有位于鼠标下方的元素才会突出显示。为了让用户明确地知道自己是怎么一路选择下来的,还需要让每一级菜单中被选择的元素突出显示。实际上,很简单,只要把.multi_drop_menu a:hover
替换成以下CSS即可。
- .multi_drop_menu li:hover > a {
- /*悬停时的文本颜色*/
- color:#fff;
- /*悬停时的背景颜色*/
- background-color:#aaa
- }
这个小小的替换能够起作用,是因为:hover
事件会沿着元素的结构层次“向上冒泡”。所以,把:hover
设定在li
元素,就相当于也把它设定给了所有祖先li
元素。然后,只要再给其子元素(链接)设定样式即可。这个改进能够极大地提高菜单的易用性,而我们的下拉菜单到此也讲完了,最终结果如图6-12所示。
图6-12 每一级菜单中的被选项都突出显示出来了
有了以上CSS文件,只要把它链接到页面中,并给一个包含无序列表的容器加上multi_drop_menu
类,该列表就会摇身一变,成为一个全功能的菜单,这个魔术我会在下一章变给你看。