让我们从零开始构建一个响应式导航栏。
结果
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
今天,我们将使用 HTML、CSS、clip-path、flexbox、网格布局等技术,从零开始构建一个响应式导航栏、一个动画汉堡图标和一个漂亮的展开效果……
阅读全文或在 YouTube 上观看我的编程演示:
结果
空白 HTML5 文档
让我们从一个空白的HTML5文档开始。这是我们的起点,我们将在接下来的章节中逐步扩展它。
你知道在 VS CODE 中输入感叹号 (!) 并按 Tab 键会生成一个空白的 HTML5 文档吗?
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Responsive Navbar</title>
</head>
<body>
<!-- content will go here -->
</body>
</html>
使用样式和外部字体
我们需要在文档中包含三个(或多或少)外部资源<head>:
- 我们自己的样式表在
styles.css。 - 我们将使用Nunito字体作为页面的默认字体。因此,该字体通过 Google Fonts 加载。我们将使用 200 和 400 两种字重。
- Font Awesome 可通过 [此处应填写链接
cdnjs.com] 获取。请注意 [integrity此处应填写属性名称] 属性,因为它是一项重要的安全功能。如果用户代理下载的文件的 SHA 校验和与 [此处integrity应填写属性名称] 属性中指定的 SHA 校验和不同,浏览器就会知道下载的内容已被篡改,因此会拒绝访问。有关子资源完整性的更多信息,请参阅这篇 MDN 文章。
<head>
....
<!-- or own styles -->
<link rel="stylesheet"
href="styles.css"
type="text/css" />
<!-- "Nunito" font via google fonts -->
<link rel="preconnect"
href="https://fonts.gstatic.com" />
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Nunito:wght@200;400&display=swap" >
<!-- Font Awesome via CDNJS -->
<link rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.2/css/all.min.css"
integrity="sha512-HK5fgLBL+xu6dm/Ii3z4xhlSUyZgTT9tuc/hSrtw6uzJOvgRr2a9jyxxT1ely+B+xFAmJKVSTbpM/CuL7qxO8w=="
crossorigin="anonymous" />
</head>
变量和基本样式
现在,styles.css让我们从文件中的一些基本内容开始。例如,变量。将我们将要使用的颜色以及一些常见的重复性内容(例如过渡设置)放在变量中,对于自定义非常有用。
请注意该nav-height变量,因为它允许我们控制导航栏的高度,许多其他元素的位置和大小都将取决于该变量。
:root {
--fg-color: rgba(255, 255, 255, 0.9);
--bg-color: #2b2b2b;
--highlight-primary: #008aff;
--gradient:
linear-gradient(300deg, #ba4aff, rgba(186, 74, 255, 0) 70%),
linear-gradient(227deg, #008aff, rgba(0, 138, 255, 0) 70%),
linear-gradient(104deg, #00ffc6, rgba(0, 255, 198, 0) 74%);
--nav-height: 3rem;
--transition: 250ms ease-out;
--transition-long: 500ms ease-out;
}
该块会重置每个元素padding,从而为浏览器和元素带来极大的一致性。marginbox-sizing
* {
padding: 0;
margin: 0;
box-sizing: border-box;
}
在根<html>元素上,我们定义了基本文本颜色和基本字体,包括字体大小。这一点非常重要,因为我们将使用很多rem值,而这些值始终与根元素上设置的字体大小相关<html>。
html {
font-family: "Nunito", sans-serif;
font-size: 18px;
font-weight: 200;
}
它<body>只是一个弹性容器,可以将内容水平居中,并将内容置于最顶部。min-height: 100vh它确保主体始终至少占据整个视口高度,从而使背景颜色覆盖整个页面。
body {
padding-top: var(--nav-height);
min-height: 100vh;
background: var(--bg-color);
color: var(--fg-color);
}
头部标记和 CSS 设置
我们先从一个简单的<header>标签开始……
<header>
</header>
……它将被定位fixed,也就是说,无论文档中的滚动位置如何,它始终将具有通过 和 提供top的left位置right。
header {
position: fixed;
top: 0;
left: 0;
right: 0;
height: var(--nav-height);
font-size: 1.5rem;
background: var(--bg-color);
color: var(--fg-color);
box-shadow: -2px 2px 8px 0px rgb(0 0 0 / 80%);
border-bottom: 1px solid var(--highlight-primary);
z-index: 1;
}
为了管理页眉内的内容流,我们将使用 CSS 网格,它会自动为每个元素生成一个新列grid-auto-flow: column。将 `grid` 属性设置为 `true`grid-auto-columns会max-content告诉 CSS 网格,每个单元格都会围绕每个元素的内容整齐排列,而不会强制换行。
header {
...
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
}
标志
左侧会显示一个标志,它其实就是一个来自 Font Awesome 的羽毛图标:
<header>
<div class="logo">
<i class="fas fa-feather-alt"></i>
</div>
</header>
该功能的样式将如下所示:
- 它被放置在容器的中心。
div.logo - 根据变量具有背景
--highlight-primary - 它的左上角和右下角都做了圆角处理,使其形状与羽毛相符。
.logo {
display: grid;
place-content: center;
padding: 0rem 1rem;
color: var(--highlight-primary);
}
.logo > i {
border-top-left-radius: 50%;
border-bottom-right-radius: 50%;
padding: 0.25rem;
background: var(--highlight-primary);
color: var(--bg-color);
}
导航栏本身
让我们在导航栏中添加一些项目。这意味着我们将使用 HTML5 上下文<nav>标签作为导航栏的容器。在导航栏内部,我们使用无序列表,并根据需要进行视觉上的调整。每个导航项的基本结构都是一个包含图标和标签的链接。
<header>
...
<nav>
<ul>
<li>
<a href="#"><i class="far fa-chart-bar"></i>Dashboard</a>
</li>
<li>
<a href="#"><i class="far fa-edit"></i>Projects</a>
</li>
<li>
<a href="#"><i class="far fa-envelope-open"></i>Posts</a>
</li>
</ul>
</nav>
</header>
为了使所有导航项并排排列,无序列表ul也像我们在头部标签中所做的那样,被设置为网格容器。两者之间有两个细微差别:
- 该
grid-template-rows属性设置为1fr,表示只有一行,并且这一行应该占用所有可用空间。 gap每个单元格之间都有一个0.5rem
header ul {
display: grid;
grid-auto-flow: column;
grid-auto-columns: max-content;
grid-template-rows: 1fr;
gap: 0.5rem;
padding: 0rem 1.5rem;
list-style-type: none;
}
列表li项再次被设置为网格容器,默认情况下,唯一的子元素(<a>)会很好地拉伸到项目的尺寸。
header ul > li {
display: grid;
padding: 0.5rem;
}
链接样式
由于我们希望每个链接的内容垂直居中,因此它是一个flex容器,并将align-items样式设置为center。其余样式设置如下:
- 加一些衬垫,并将边角略微打磨圆润。
- 设置文本和背景颜色
- 移除链接中不必要的下划线样式
- 使背景颜色可过渡,这对于悬停样式非常有用。
每个链接中的图标字体略微缩小,以便与实际文本大小保持良好的比例。
header a {
display: flex;
align-items: center;
padding: 0rem 1.5rem;
border-radius: 0.25rem;
color: var(--fg-color);
background-color: rgba(0, 0, 0, 0.1);
text-decoration: none;
transition: background-color var(--transition);
}
header a > i {
margin-right: 0.5rem;
color: var(--highlight-primary);
font-size: 1rem;
transition: color var(--transition);
}
因此,在悬停样式中,链接的背景颜色更改为高亮颜色,图标的颜色也更改为较深的背景色:
header a:hover {
background-color: var(--highlight-primary);
}
header a:hover > i {
color: var(--bg-color);
}
汉堡菜单按钮
当屏幕太小无法容纳所有导航项时,菜单项将被放置在右上角的单独导航栏中。然而,届时我们需要某种触发器或按钮,以便在再次点击时显示或隐藏菜单。因此,我们将使用复选框,因为我们可以通过:checked伪类来响应其状态。由于复选框的样式设置可能非常繁琐,我们将使用一个<label>通过属性引用复选框的标签for。这允许我们将所有需要的样式都应用到标签上,并且由于该for属性包含与复选框相同的 ID,因此点击标签也会切换复选框本身的状态。这样,我们就可以安全地隐藏复选框而不会损失功能(此处暂不讨论可访问性——我将在另一篇文章中讨论)。
<header>
<div class="logo">...</div>
<input type="checkbox" class="toggle" id="nav-toggle">
<label for="nav-toggle" id="nav-toggle-label">
...
</label>
<nav>...</nav>
</header>
因此,如上所述,标签和复选框默认情况下是隐藏的,因为我们默认导航项有足够的空间。
#nav-toggle-label {
display: none;
cursor: pointer;
}
#nav-toggle {
display: none;
}
通过使用@media查询,我们现在可以确定视口的状态,我们将其定义为太小而无法显示导航项。在我们的例子中,如果屏幕/视口尺寸小于768px宽度,则会应用某些样式来改变导航栏的外观和行为。
@media screen and (max-width: 768px) {
/*
styles which are applied only if the page is
rendered on a screen (e.g. not printed) and
the viewport's width is less thant 768px wide
*/
}
我们首先要做的是将该nav元素定位得类似于标题fixed,但不要将其固定在视口的顶部边缘,而是固定在右侧:
@media screen and (max-width: 768px) {
header nav {
position: fixed;
top: 0;
bottom: 0;
width: 24rem;
right: 0rem;
padding-top: var(--nav-height);
background: var(--gradient);
box-shadow: -2px 2px 8px 0px rgb(0 0 0 / 80%);
transition: clip-path var(--transition-long),
background-color var(--transition-long);
}
}
ul现在我们还需要通过grid-auto-flow从column到 来改变无序列表内部网格的流向row。这样,导航项就会垂直排列。
@media screen and (max-width: 768px) {
header ul {
grid-auto-flow: row;
grid-template-columns: 1fr;
grid-template-rows: none;
grid-auto-rows: max-content;
gap: 0.5rem;
padding: 0;
}
}
其余样式稍微改变了链接的对齐方式(左对齐place-content),并改变了默认状态和悬停状态下的颜色。
@media screen and (max-width: 768px) {
header a {
place-content: flex-start;
padding: 0.5rem 1.5rem;
}
header a > i {
color: var(--bg-color);
}
header a:hover {
background-color: var(--bg-color);
}
header a:hover > i {
color: var(--highlight-primary);
}
}
菜单按钮
最后,是时候设置菜单按钮的样式了。首先,我们来配置header标签,使徽标保持在左侧,菜单按钮移到右侧,这可以通过设置justify-content以下值来实现space-between:
@media screen and (max-width: 768px) {
header {
justify-content: space-between;
align-items: center;
}
}
要创建一个菜单按钮,使其能够从带有菜单图标(三个堆叠在一起的横条)的按钮过渡到带有关闭图标的按钮,我们只需div.bars在 中放入三个label。
<header>
...
<label for="nav-toggle" id="nav-toggle-label">
<div class="bar"></div>
<div class="bar"></div>
<div class="bar"></div>
</label>
...
</header>
那么,我们先来做一些准备工作:
- 该
--size变量控制菜单按钮的大小,并与导航栏的高度成正比。 - 该
--bar-height变量告诉每个柱状图它应该有多高。 column这些条形图是通过使用 flex-box 沿方向堆叠排列的,space-between使得所有条形图均匀分布在包含元素的高度上。
@media screen and (max-width: 768px) {
#nav-toggle-label {
--size: calc(var(--nav-height) / 3);
--bar-height: 2px;
display: flex;
flex-direction: column;
justify-content: space-between;
flex-basis: auto;
width: var(--size);
height: var(--size);
margin-right: calc(var(--nav-height) / 3);
z-index: 2;
}
每个柱状图本身都只是简单地设置了--bg-color背景颜色,并使用了--bar-height上面提到的变量。然后width: 100%;,它会被拉伸以占据所有可用的水平空间。
#nav-toggle-label .bar {
display: inline-block;
height: var(--bar-height);
width: 100%;
background-color: var(--bg-color);
transition: transform 250ms ease-out;
}
}
现在有趣的部分来了,我们要定义如何将三个横条重新排列成一个关闭图标。每个选择器都绑定到:checked复选框的伪类,因此如果复选框未被选中,则这些样式不会应用。
- 最上面的横条旋转了 225 度,并向下移动到包含元素的中心。
- 最下面的横杆旋转了 135 度(225 度减去 90 度),并向上移动到包含元素的中心。
- 只需将中间元素沿 x 轴缩放至零,即可使其不可见。
@media screen and (max-width: 768px) {
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(1) {
transform:
translate(0, calc(var(--size) / 2 - var(--bar-height) / 2))
rotate(225deg);
}
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(2) {
transform: scaleX(0);
}
#nav-toggle:checked + #nav-toggle-label > .bar:nth-child(3) {
transform:
translate(0, calc(-1 * var(--size) / 2 + var(--bar-height) / 2))
rotate(135deg);
}
}
展开和折叠导航菜单
我们快完成了!现在我们需要将导航栏简化成一个小圆圈,如果复选框未选中,则将其放置在菜单图标的正后方。因此,我们使用一个clip-path半径为1rem导航栏高度三分之一的圆,圆心位于视口的右上角,但略微向左下方偏移,以匹配导航栏的高度。
@media screen and (max-width: 768px) {
header nav {
...
clip-path: circle(
calc(var(--nav-height) / 3)
at
calc(100% - var(--nav-height) / 2)
calc(0% + var(--nav-height) / 2)
);
}
}
然后,将这个圆扩展到视口较大边缘的 125%(125vmax)。这个vmax单位非常方便,因为它会动态地将基准尺寸更改为视口的宽度或高度——具体取决于两者中哪个更大。因此,较大边缘的 125% 足以覆盖视口的高度。
@media screen and (max-width: 768px) {
#nav-toggle:checked + * + nav {
clip-path: circle(125vmax at 100% 0%);
background-color: var(--bg-color);
}
}
最后,我们还对该clip-path属性进行了平滑过渡处理,使菜单的展开和折叠更加流畅。就这样,就完成了!
@media screen and (max-width: 768px) {
header nav {
...
transition: clip-path var(--transition-long),
background-color var(--transition-long);
}
}