WebGL Tutorial
and more

Mac风格暗黑菜单

撰写时间:2025-03-19

修订时间:2025-03-19

本文中,我们仅使用CSS,一步步制作出一个精美的MacOS X系统风格的暗黑主题菜单。

这是最终的效果

编写HTML结构内容

body { color: #ACB7C4; background-color: #2B2B2B; }

为避免太刺眼,上面的CSS仅设置了一个柔和的暗黑色调,下面还要进一步修改。

运行结果为多级缩进文本。

最深层的DOM结构如下:

  • nav.menu-bar
    • ul [.menu .top-level]
      • li.menu-item 文件
        • ul.submenu
          • li [.menu-item .icon-new] 新建
          • li [.menu-item .icon-open] 打开
          • li.divider
          • li.menu-item 导出
            • ul.submenu
              • li.menu-item 导出为PDF
    • ul [.menu .top-level] 编辑
    • ul [.menu .top-level] 帮助

body的整体设置

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } /* temporary color setting */ body { color: #ACB7C4; }

取消ul的前置图形

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } /* temporary color setting */ body { color: #ACB7C4; } /* ----------- new added ----------- */ nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } }

上面还加大了菜单区域的底边距,以与下面的文本相隔开。

菜单条的设置

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } /* temporary color setting */ body { color: #ACB7C4; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } /* ----------- new added ----------- */ .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; }

.menu-bar的标签的display设置为flex,则原来竖排的3个顶层菜单项立即改为横排。

菜单项的设置

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } /* ----------- new added ----------- */ .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; /* temporay setting */ border: 1px solid gray; } p { color: #E0E0E0; }

菜单项用于显示菜单的文本,以及作为管理下级子菜单的父容器。因此可在其上面设置具体的颜色。因此之前临时设置的前景颜色可去掉了。

为清晰地看到各菜单项的范围,上面为每个菜单项临时加了边框线。可以看出,各个子菜单的区域都以默认的竖排方式,整齐地排在顶层菜单项的内部。

子菜单的设置

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } /* ----------- new added ----------- */ .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); }

3个顶层菜单项不需要画边框,除此之外的所有子菜单都需要画边框。因此改由设置边框线.submemu标签绘制边框线。

.submemuposition设置为absolute后,顶层菜单下项的所有子标签都脱离了原来的流布局,改为以绝对方式在父容器中定位。而父容器原来因存放子标签的内容而被撑开的空间也得以收回,因此顶层3个菜单项仅因第37行代码padding: 8px 15px;的设置而左右相邻15px个单位摆放。

而各个子菜单,包括它们所下属的子标签的区域,均以

top: 100%; left: 0;

的方式,在各自的父容器中绝对定位,并统一设置了200px的宽度值。

这里有个小技巧。top的值,如果改为0,则子容器的顶边框线与父容器的顶边框线对齐;改为100%,则子容器的顶边框线与父容器的底边框线对齐。

消隐子菜单

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); /* ----------- new added ----------- */ transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; }

鼠标移动时显示子菜单

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } /* ----------- new added ----------- */ .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; }

首先,选择器.menu-item:hover > .submenu确保只打开鼠标悬停所在标签的直接子标签,子标签的子标签不会被打开。

其次,关于为何需要同时设置opacityvisibility的问题。

visibility设置标签是否可见。其值只有01两种。为其值为0时,用户看不见,也无法用鼠标点击它;当其值为1时,用户可以看见,且可以用鼠标点击它。因此,此值刚开始应设置为0,当用户移动鼠标时,则鼠标所在的标签下的所有子菜单应设置其值为1

但若此值的动画时间设置过长,则可能会因为前面显示的区域来不及关闭而导致鼠标在子标签区域左右移动时,很容易从从顶层菜单项文件的子菜单直接串位到编辑帮助的子菜单的奇怪现象。

所以visibility值必须设置。且在其上设置transition的值应越小越好。

上述特点让我们失去了定制动画时长的特权。借助于opacity,我们可以通过单独设置其不透明度的动画时长,让标签呈现出淡进淡出的效果。其状态是立即打开或立即关闭,但其视觉过程却可以由我们来随意控制。子弹是飞得很快,但在录像机上,我们可以看十倍的慢镜头。将opacity的值动画时长设置为0.11秒,均是不错的选择。

设置级联子菜单

共需两步。

将第二级以上的子菜单向右推开

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } /* ----------- new added ----------- */ .submenu .submenu { top: 0px; left: 100%; }

如上所述,将left的值设置为100%,可使子菜单的左边框与父容器的右边框自动对齐。

添加右边还有内容的提示

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } .submenu .submenu { top: 0px; left: 100%; } /* ----------- new added ----------- */ .submenu>.menu-item:has(.submenu)::after { content: ">"; float: right; margin-right: 0.2em; color: #888; display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; } .submenu .menu-item { margin: 0.1em; }

这里使用了CSS Selectors Level 4中的:has的伪类选择器。虽然还是草案,但主流浏览器支持得很好。

分隔条

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } .submenu .submenu { top: 0px; left: 100%; } .submenu>.menu-item:has(.submenu)::after { content: ">"; float: right; margin-right: 0.2em; color: #888; display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; } .submenu .menu-item { margin: 0.1em; } /* ----------- new added ----------- */ .divider { border-bottom: 1px solid #555; margin: 6px 0; }

快捷键助词符

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } .submenu .submenu { top: 0px; left: 100%; } .submenu>.menu-item:has(.submenu)::after { content: ">"; float: right; margin-right: 0.2em; color: #888; display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; } .submenu .menu-item { margin: 0.1em; } .divider { border-bottom: 1px solid #555; margin: 6px 0; } /* ----------- new added ----------- */ .shortcut { color: #888; float: right; margin-right: 0.5em; font-size: 0.9em; opacity: 0.9; }

向右浮动,从右边吸附对齐,再淡化一些不透明度。

被屏蔽的菜单项

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } .submenu .submenu { top: 0px; left: 100%; } .submenu>.menu-item:has(.submenu)::after { content: ">"; float: right; margin-right: 0.2em; color: #888; display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; } .submenu .menu-item { margin: 0.1em; } .divider { border-bottom: 1px solid #555; margin: 6px 0; } .shortcut { color: #888; float: right; margin-right: 0.5em; font-size: 0.9em; opacity: 0.9; } /* ----------- new added ----------- */ .disabled { color: #666; cursor: not-allowed; }

cursor设置为not-allowed

菜单项图标

* { margin: 0; padding: 0; box-sizing: border-box; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif; } body { background: #2A2A2A; min-height: 100vh; padding: 20px; } nav.menu-bar { margin-bottom: 5em; ul { list-style-type: none; margin: 0; padding: 0; } } .menu-bar { background: hsl(300, 20%, 30%); border-radius: 8px; padding: 8px 20px; z-index: 1000; display: flex; position: relative; margin: 1em; } .menu-item { position: relative; padding: 8px 15px; color: #E0E0E0; cursor: pointer; font-size: 14px; transition: all 0.1s; white-space: nowrap; } p { color: #E0E0E0; } .top-level > .menu-item { border-radius: 4px; } .submenu { position: absolute; top: 100%; left: 0; z-index: 1001; min-width: 200px; border: 1px solid gray; border-radius: 6px; background: #3D3D3D; box-shadow: 0 5px 15px rgba(0, 0, 0, 0.3); transition: visibility 0.1s; transition: opacity 1s; visibility: hidden; opacity: 0; } .menu-item:hover { background: #444; } .menu-item:hover > .submenu { visibility: visible; opacity: 1; } .submenu .submenu { top: 0px; left: 100%; } .submenu>.menu-item:has(.submenu)::after { content: ">"; float: right; margin-right: 0.2em; color: #888; display: flex; justify-content: center; align-items: center; width: 16px; height: 16px; } .submenu .menu-item { margin: 0.1em; } .divider { border-bottom: 1px solid #555; margin: 6px 0; } .shortcut { color: #888; float: right; margin-right: 0.5em; font-size: 0.9em; opacity: 0.9; } .disabled { color: #666; cursor: not-allowed; } /* ----------- new added ----------- */ .menu-item[class*=" icon-"]::before { content: ''; display: inline-block; width: 16px; height: 16px; margin-right: 8px; vertical-align: middle; background-repeat: no-repeat; background-position: center; background-size: contain; } .icon-new::before { background-image: url('data:image/svg+xml;utf8,'); } .icon-open::before { background-image: url('data:image/svg+xml;utf8,'); } .icon-save::before { background-image: url('data:image/svg+xml;utf8,'); }

对于类选择器中带有 icon-字符串的标签,使用::before伪类选择器在它们前面添加一个16px × 16px的方框,设置其右边距,居中对齐,然后再单独为它们添加各自的背景图像。

在绘制图像时,使用了data:URL的协议,直接将SVG代码嵌入其中,无比便捷。本站的SVG Live Editor可方便地绘制此类图形。

参考资源

  1. CSS Selectors Level 4