6.1 导航菜单

菜单由一组链接组成。用HTML中的列表元素(ulol)来分组链接不仅符合逻辑,而且即使没有额外的CSS也能适当显示链接的层次。默认情况下,由于列表项(li)是块级元素,因此它们会上下堆叠。

6.1.1 纵向菜单

先从最简单的情况开始讲,通过下面这个简单的例子,你会知道把列表转换成菜单的关键技术。

  1. <nav class="list1">
  2. <ul>
  3. <li><a href="#">Alternative</a></li>
  4. <li><a href="#">Country</a></li>
  5. <li><a href="#">Jazz</a></li>
  6. <li><a href="#">Rock</a></li>
  7. </ul>
  8. </nav>

以上标记给每个列表项的文本都加了链接标签,让它们都变得可以点击。(每个链接中的#是URL占位符,将来可以替换成真正的URL。)

首先,我们通过第一个例子来展示几个问题,很多初学者在为导航链接写样式的时候都会遇到这些问题。然后,再告诉你怎么解决它们。无论如何,这些CSS应用到HTML之后,会得到一个看起来很简洁的菜单,如图6-1所示。

  1. /*去掉默认的内边距和外边距*/
  2. * {margin:0; padding:0;}
  3. /*设定本例中菜单的大小和位置*/
  4. nav {margin:50px; width:150px;}
  5. /*给菜单加上边框*/
  6. .list1 ul {border:1px solid #f00; border-radius:3px;
  7. padding:5px 10px 3px;}
  8. /*去掉项目符号并为链接添加间距*/
  9. .list1 li {list-style-type:none; padding:3px 10px;}
  10. /*“非首位子元素”选择符*/
  11. .list1 li + li {border-top:1px solid #f00;}
  12. /*为链接添加样式*/
  13. .list1 a {text-decoration:none; font:20px Exo, helvetica,
  14. arial, sans-serif; font-weight:400; color:#000;
  15. background:#ffed53;}
  16. /*悬停高亮*/
  17. .list1 a:hover {color:#069;}

enter image description here图6-1 应用样式之后的无序列表,从箭头光标可知,没有文本的区域是不能点击的

添加这些样式没什么难度。HTML5新增的nav元素在语义上很恰当,因此我们用它作为导航菜单的容器,还给它添加了一个类作为设定样式的上下文。

使用“非首位子元素”选择符

前面的CSS代码中有两点特别值得一说。首先,加粗的li + li选择符的意思是“任何跟在li之后的li”。

对于连续的元素——比如这里多个li,这个选择符会选择除第一个之外的所有li元素。这样,就可以给除第一个li之外的所有列表项上方加一条边框。假如我只简单使用li选择符,那第一个列表项上方也将被加上边框,而这不是我们想要的。我把这种选择符称为“非首位子元素”选择符,它在选择列表时非常实用。当然,也有实现相同效果的其他方法,比如:

  1. /*给所有li上方添加一条边框*/
  2. li {border-top:1px solid #f00;}
  3. /*去掉第一个li上方的边框*/
  4. li:first-child {border-top:none;}

让列表行可以点击

在图6-2中,我为每个链接都加上了背景色,以便显示相应链接可点击区域的大小。如图所示,目前只有文本是可以点的,因为链接(a)是行内元素,它会收缩并包住其中的文本。然而,更好的用户体验是让列表项所在的整行都能点击。之所以要重点讲这个问题,是因为我发现很多站点都没有这么做。结果就是用户必须用鼠标点击文本,点击文本之外的地方没有反应。

此外,在每个li元素的上方和下方,各有3像素的内边距,导致链接文本与列表项边框之间有了距离。正因为有了这段距离,当用户鼠标移动到链接间的间隙时,光标会从“悬停于链接之上”时的小手形状,变成常规的箭头形状。

enter image description here图6-2 临时加的背景色让我们看清了a元素实际可以点击的区域大小,如图所示,光标在经过两个链接的间隙时从小手变成了指针

要解决上述问题,首先得把内边距从li元素转移到链接内部,然后让链接完全填满整个列表项。

  1. * {margin:0; padding:0;}
  2. nav {margin:50px; width:150px;}
  3. .list1 ul {border:1px solid #f00; border-radius:3px;
  4. padding:5px 10px 3px;}
  5. .list1 li {list-style-type:none; padding:3px 10px;}
  6. .list1 li + li a {border-top:1px solid #f00;}
  7. .list1 a {display:block; padding:3px 10px; textdecoration:
  8. none; font:20px Exo, helvetica, arial, sansserif;
  9. font-weight:400; color:#000; background:#ffed53;}
  10. .list1 a:hover {color:#069;}

把选择符li + li变成li + li a,就可以把上边框添加到列表项后面的列表项所包含的链接元素上(虽然有点绕,但你应该明白我的意思吧?)。而且,我还把内边距从列表项转移到了链接内部。这样,链接上下就互相接触了,中间没了间隙,鼠标经过时光标状态也不会变了。最后,还要把每个链接的display属性设定为block,这样链接元素的盒子就会由“收缩包围内容”变成“扩展填充父元素”。换句话说,链接的可点击区域将扩大到整个列表行。图6-3清晰地说明了经过这些改进后的效果。

enter image description here图6-3 每个链接都填满了整个列表项,所以整个区域都可以点击了。左图的背景色显示了可点击区域,而右图则显示了去掉测试用的背景色之后的最终效果

6.1.2 横向菜单

默认情况下,列表项是垂直堆叠在一起的。不过,要把它们变成水平排列的横向菜单也十分容易。方法就是浮动列表项。比如下面这个简单的列表:

  1. <nav class="list1">
  2. <ul>
  3. <li><a href="#">Shirts</a></li>
  4. <li><a href="#">Pants</a></li>
  5. <li><a href="#">Dresses</a></li>
  6. <li><a href="#">Shoes</a></li>
  7. <li><a href="#">Accessories</a></li>
  8. </ul>
  9. </nav>

和下面这些与前面创建纵向菜单很相似的CSS:

  1. .list1 ul {
  2. /*强制ul包围浮动的li元素*/
  3. overflow:hidden;
  4. }
  5. .list1 li {
  6. /*让li元素水平排列*/
  7. float:left;
  8. /*去掉项目符号*/
  9. list-style-type:none;
  10. }
  11. .list1 a {
  12. /*让链接填满li元素*/
  13. display:block;
  14. padding:0 16px;
  15. /*去掉链接的下划线*/
  16. text-decoration:none;
  17. color:#999;
  18. }
  19. .list1 li + li a {border-left:1px solid #aaa;}
  20. .list1 a:hover {color:#555;}

enter image description here图6-4 简单的水平菜单

图6-4这个水平菜单是不是很常见?没错,很多时尚的零售网店都有它们的身影。请容我啰嗦几句,解释解释上面的CSS代码:浮动让li元素从垂直变成水平,display:block让链接从收缩变成扩张,从而整个li元素都变成了可以点击的。另外,选择符li + li a为除第一个链接之外的每个链接左侧都加了一条竖线,作为视觉分隔线。好啦,可以讲更复杂的样式了。

6.1.3 下拉菜单

在我收到的邮件中,询问下拉菜单问题的最多。所以,本节我就来给大家彻底讲清楚,看一看到底什么是下拉菜单,怎么创建下拉菜单,下拉菜单有什么变化形式。下拉菜单是以一组嵌套列表为基础,综合运用我们刚刚学到的纵向和横向菜单的CSS技术创建的。图6-5展示了一个下拉菜单。

enter image description here图6-5 三级下拉菜单

为方便设定菜单样式,我为列表容器nav添加了multi_drop_menu类。这个菜单的每一条CSS规则都以.multi_drop_menu开头,以确保它们只会应用给带有这个类的容器。

下拉菜单的标记是在前面菜单例子基础上扩充而来的。下面就是所有标记,虽然看起来挺吓人,但实际上不过是一个简单的三层嵌套列表而已。

  1. <nav class="multi_drop_menu">
  2. <!-- 一级开始 -->
  3. <ul>
  4. <li><a href="#">Power</a></li>
  5. <li><a href="#">Money</a></li>
  6. <li><a href="#">Love</a></li>
  7. <li><a href="#">Fame</a>
  8. <!-- 二级开始 -->
  9. <ul>
  10. <li><a href="#">Sports Star</a></li>
  11. <li><a href="#">Movie Star</a></li>
  12. <li><a href="#">Rock Star</a>
  13. <!-- 三级开始 -->
  14. <ul>
  15. <li><a href="#">Bruce Springsteen</a></li>
  16. <li><a href="#">Bono</a></li>
  17. <li><a href="#">Mick Jagger</a></li>
  18. <li><a href="#">Bob Dylan</a></li>
  19. </ul>
  20. <!-- 三级结束 -->
  21. </li>
  22. <li><a href="#">Web Designer</a></li>
  23. </ul>
  24. <!-- 二级结束 -->
  25. </li>
  26. </ul>
  27. <!-- 一级结束 -->
  28. </nav>

以上标记只包含图6-5中展示的菜单项。想嵌套更多子菜单也非常简单,方法是在标记中找到相应链接,把包含子菜单项的无序列表加在链接之后、包含它的li元素结束标签之前。一会儿你就知道了,这样做无须更改CSS。下面就从创建水平的顶级菜单开始吧。

顶级菜单

以下是顶级菜单的CSS(请配合注释理解它们的作用):

  1. /*添加视觉样式*/
  2. .multi_drop_menu {font:1em helvetica, arial, sans-serif;}
  3. .multi_drop_menu a {
  4. /*让链接充满列表项*/
  5. display:block;
  6. /*文本颜色*/
  7. color:#555;
  8. /*背景颜色*/
  9. background-color:#eee;
  10. /*链接的内边距*/
  11. padding:.2em 1em;
  12. /*分隔线宽度*/
  13. border-width:3px;
  14. /*可以有颜色,也可以透明*/
  15. border-color:transparent;
  16. }
  17. .multi_drop_menu a:hover {
  18. /*悬停时文本颜色*/
  19. color:#fff;
  20. /*悬停时背景色*/
  21. background-color:#aaa;
  22. }
  23. .multi_drop_menu a:active {
  24. /*点击时背景变色*/
  25. background:#fff;
  26. /*点击时文本变色*/
  27. color:#ccc;
  28. }
  29. /*添加功能样式*/
  30. .multi_drop_menu * {margin:0; padding:0;}
  31. /*强制ul包围li*/
  32. .multi_drop_menu ul {float:left;}
  33. .multi_drop_menu li {
  34. /*水平排列菜单项*/
  35. float:left;
  36. /*去掉默认的项目符号*/
  37. list-style-type:none;
  38. /*为子菜单提供定位上下文*/
  39. position:relative;
  40. }
  41. .multi_drop_menu li a {
  42. /*让链接填充列表项*/
  43. display:block;
  44. /*给每个链接添加一个右边框*/
  45. border-right-style:solid;
  46. /*背景只出现在内边距区域后面*/
  47. background-clip:padding-box;
  48. /*去掉链接的下划线*/
  49. text-decoration:none;
  50. }
  51. .multi_drop_menu li:last-child a {border-right-style:none;}
  52. /*临时隐藏低级菜单*/
  53. .multi_drop_menu li ul {display:none;}

enter image description here图6-6 顶级菜单项,此刻鼠标正按住未释放,因此有:active效果

对于图6-6的CSS样式,首先要注意的是,我把菜单的视觉样式与功能样式分开来写了。视觉样式控制字体大小、边框和文本的颜色,功能样式控制菜单的布局和行为。两者的区别通过代码注释是很容易分辨的。对于下拉菜单这么复杂的组件,分开来写视觉和功能代码是非常值得提倡的。这样,将来如果有人要修改菜单外观,只要修改它的视觉样式就好了,而功能样式可以原封不动。

如果你在代码中像这样把视觉样式与功能样式分开来写,那一定要用注释说明为什么。

目前,最明显的变化还是li通过浮动由垂直堆叠变成了水平排列。至于为了让ul包围列表项,没有使用overflow:hidden,而是使用了float:left,是因为前者会导致后来添加到下拉菜单中的子菜单无法显示——它们最终会显示在父元素ul的外面,结果会因为“溢出”(overflow)而被隐藏(hidden)

为了保证用户体验,所有视觉样式——内边距、背景、边框,等等,都要应用给链接a,而不要应用给ulli,以便热区(可点击区域)最大化,让用户鼠标经过时不会产生前面例子中看到的状态切换。为达到这个目的,同时还要从视觉上分隔链接,我们使用了background-clip:padding-box声明,这样就可以阻止链接的背景(像常规状态下一样)延伸到边框后面。然后,让边框透明(也可显示为其他实色),在链接之间产生间隙,让后面的页面能够透过边框被看到。如此一来,不用外边距也能分隔链接,而且鼠标从一个菜单项移动到另一个菜单项时,也不会出现光标切换。菜单项之间从视觉上是分开,但实际上却是紧挨在一块的。

要了解background-clip和透明边框的更多用法,可以阅读这篇文章:http://css-tricks.com/transparent-borders-with-background-clip

或许你注意到了,我给li元素应用了position:relative,这是给添加子菜单作准备的,在当前这一步里实际上它没有什么效果。另外,最后一行CSS隐藏了子菜单,以防显示它们影响我们创建顶级菜单。在后面两步中,我会告诉你怎么在菜单项处于悬停状态时显示它们。

第一步为顶级菜单添加的样式都会被次级菜单继承。换句话说,需要应用给子菜单的样式大部分都已经写完啦!

菜单的下拉部分

虽然子菜单继承顶级菜单的样式为我们提供了方便,但其实有些样式在子菜单中并不是必要的。比如,(列表的第二级)下拉菜单是垂直堆叠的,所以我们不希望其中的列表项继承浮动而变成水平排列。另外,顶级菜单使用右边框作为分隔线,而下拉菜单需要使用上边框来分隔垂直堆叠的菜单项。这一步,我们还会绝对定位下拉菜单,以便它能精确地对齐父元素(包含子菜单的li),而这个父元素已经在上一步被设定成相对定位了。

与此同时,为了看到这些设定的效果,还要显示二级下拉菜单,并继续隐藏三级下拉菜单(下一步再考虑它)。

以下是实现上述改变需要添加的CSS:

  1. /* 添加的视觉样式 */
  2. /*二级菜单宽度*/
  3. .multi_drop_menu li ul {width:9em;}
  4. .multi_drop_menu li li a {
  5. /*去掉继承的右边框*/
  6. border-right-style:none;
  7. /*添加上边框*/
  8. border-top-style:solid;
  9. }
  10. /* 添加的功能样式 */
  11. .multi_drop_menu li ul {
  12. /*临时显示二级下拉菜单*/
  13. display:block;
  14. /*相对于父菜单项定位*/
  15. position:absolute;
  16. /*左边与父菜单项对齐*/
  17. left:0;
  18. /*顶边与父菜单项底边对齐*/
  19. top:100%;
  20. }
  21.  
  22. .multi_drop_menu li li {
  23. /*停止浮动,恢复堆叠*/
  24. float:none;
  25. }
  26. .multi_drop_menu li li ul {
  27. /*继续隐藏三级下拉菜单*/
  28. display:none;
  29. }

enter image description here图6-7 二级下拉菜单已经显示在其父菜单项下方

这一步成功的关键是下拉菜单的绝对/相对定位。通过将其顶边位置(top)设定为100%(相对于其相对定位的父元素li),其顶边会与父元素底边恰好对齐。它与父元素之间的间隙,实际上是下拉菜单中第一个链接的边框,参见图6-7。

让下拉菜单响应鼠标事件

接下来是好玩的部分了,即要让下拉菜单拥有自己的功能。换句话说,它一开始是隐藏的,只有在其父元素处于鼠标悬停状态时,才需要显示。

  1. .multi_drop_menu li ul {
  2. /*隐藏二级下拉菜单*/
  3. display:none;
  4. /*相对于父菜单项定位*/
  5. position:absolute;
  6. /*左边与父菜单项对齐*/
  7. left:0;
  8. /*顶边与父菜单项底边对齐*/
  9. top:100%;
  10. }
  11. .multi_drop_menu li:hover > ul {
  12. /*父元素悬停时显示*/
  13. display:block;
  14. }

enter image description here图6-8 鼠标移动到链接上,其子菜单就会显示出来

让菜单起作用的关键在于先把它藏起来:

  1. /*隐藏二级菜单*/
  2. li ul {display:none;}

然后,再在父元素悬停时把它显示出来:

  1. /*显示二级菜单*/
  2. li:hover > ul {display:block;}

最后一行CSS的意思是:当鼠标移动到列表项上时,就显示它的子列表。注意,这里的:hover触发器是设定在li元素而非链接自身上的。这样做是因为我们想要显示li的子元素ul,而它不是想链接的子元素。此外,为了只显示其子元素,悬停列表项与子列表之间还有一个子选择符>,结果如图6-8所示。如果没有这个子选择符,当顶级菜单项处于悬停状态时,会同时显示二级和三级菜单。

添加三级菜单

接下来我们就趁热打铁,把三级菜单也弄好吧。实际上,这时候三级菜单已经算是能用了。不信?找到图6-8的示例代码,在浏览器中打开,然后用鼠标触发子菜单,就会看到图6-9所示的效果。

enter image description here图6-9 在父元素处于鼠标之下时,三级菜单也会显示,只不过位置不对

由于前面包含:hover的CSS规则会像应用给二级菜单一样,应用给三级菜单,所以在父元素处于鼠标之下时,三级菜单自然也会显示出来。可是,图6-9中的三级菜单被其父元素挡住了。这时候三级菜单与其二级父元素的位置是什么关系?没错,就是二级菜单与顶级菜单的关系。所以,三级菜单跑到了鼠标下的二级菜单后面去了,而且其第一项顶边与悬停的父菜单项底边是对齐的。从图中可以看到,三级菜单第一项“Bruce Springsteen”的上半部分被二级菜单最后一项“Web Designer”给盖住了。

我们在这一步要做的,就是把三级菜单放到二级菜单右侧,让它的顶边与鼠标所在菜单项的底边对齐。

  1. .multi_drop_menu li li ul {
  2. /*相对于父菜单定位*/
  3. position:absolute;
  4. /*与父菜单右侧对齐*/
  5. left:100%;
  6. /*与父菜单项顶边对齐*/
  7. top:0;
  8. }

enter image description here图6-10 三级下拉菜单显示在了二级菜单旁边

现在的菜单能用了,参见图6-10。你可以在顶级添加更多列表项,在标记中添加更多子列表,然后无须多写一行CSS,它们就能构成新的下拉菜单。此时,我还想再作两个调整。首先,为了真正让这些代码有用,而且可以重用,需要再写一些样式,让顶级菜单能够垂直显示,以便能将其用在导航侧边栏里。为此,我得先给nav容器添加第二个类vertical

  1. <!-- HTML类名之间要有空格 -->
  2. <nav class="multi_drop_menu vertical">

这样,我就可以写一些样式,让它们只在导航容器有vertical类的情况下才起作用。所以,这些新增规则的选择符开头都是

  1. /*CSS类名之间没有空格*/
  2. .multi_drop_menu.vertical

这意味着选择一个带有两个类的元素——注意,CSS的类名间没有空格。

  1. /*顶级垂直菜单宽度*/
  2. .multi_drop_menu.vertical {width:8em;}
  3. .multi_drop_menu.vertical li a {
  4. border-right-style:none;
  5. border-top-style:solid;
  6. }
  7. .multi_drop_menu.vertical li li a {border-left-style:solid;}
  8. .multi_drop_menu.vertical ul,
  9. .multi_drop_menu.vertical li {
  10. /*让顶级菜单垂直显示*/
  11. float:none;
  12. }
  13. .multi_drop_menu.vertical li ul {
  14. /*子菜单左边与上一级菜单右边对齐*/
  15. left:100%;
  16. /*子菜单顶边与上一级菜单项顶边对齐*/
  17. top:0;
  18. }

为了让菜单恢复默认的堆叠状态,这里重置了顶级li元素及其父元素ul的浮动属性,后者原来浮动是为了包围浮动的li元素。

这里还为nav容器设定了宽度。如果不设定导航容器宽度,那么nav及其包含的顶级菜单项都会尽可能伸展。最后,我们把二级菜单与顶级菜单的位置关系,设定成了前面三级菜单和二级菜单之间的关系,即让子菜单顶边与父菜单项顶边对齐。当然,还去掉了顶级菜单项的右边框,代之以上边框,如图6-11所示。

enter image description here图6-11 增加了垂直菜单样式后,顶级菜单项上下堆叠起来了

突出显示选择路径

图6-11显示,只有位于鼠标下方的元素才会突出显示。为了让用户明确地知道自己是怎么一路选择下来的,还需要让每一级菜单中被选择的元素突出显示。实际上,很简单,只要把.multi_drop_menu a:hover替换成以下CSS即可。

  1. .multi_drop_menu li:hover > a {
  2. /*悬停时的文本颜色*/
  3. color:#fff;
  4. /*悬停时的背景颜色*/
  5. background-color:#aaa
  6. }

这个小小的替换能够起作用,是因为:hover事件会沿着元素的结构层次“向上冒泡”。所以,把:hover设定在li元素,就相当于也把它设定给了所有祖先li元素。然后,只要再给其子元素(链接)设定样式即可。这个改进能够极大地提高菜单的易用性,而我们的下拉菜单到此也讲完了,最终结果如图6-12所示。

enter image description here图6-12 每一级菜单中的被选项都突出显示出来了

有了以上CSS文件,只要把它链接到页面中,并给一个包含无序列表的容器加上multi_drop_menu类,该列表就会摇身一变,成为一个全功能的菜单,这个魔术我会在下一章变给你看。