我们都知道什么是 汉堡菜单,对吧?当这种模式开始进入网页设计时,它既被嘲笑也被赞扬,因为它极简主义的设计可以让主菜单隐藏在屏幕外,尤其是在移动设备上,每个像素的空间都很重要。

无论喜欢与否,汉堡菜单都已存在,并且可能会持续一段时间。问题是如何实现它们。当然,它们看起来简单直观,但实际上并非如此。例如,是否应该与标签配对?它们在屏幕的左侧还是右侧更有效?我们如何处理关闭这些菜单,是通过点击还是触摸?图标应该是一个 SVG、字体、Unicode 字符还是纯 CSS?如何处理 无肉选项?
我想构建其中一个,但找不到简单的解决方案。大多数解决方案都基于库,例如 reactjs-popup 或 react-burger-menu。它们很棒,但更适合复杂的解决方案。那么,一个简单的三线菜单的核心用例是什么呢?它在点击时从屏幕侧面滑出一个面板,然后在再次点击时将面板滑回?
我决定自己构建一个简单的汉堡菜单,配有侧边栏。没有泡菜、洋葱或番茄酱。只有肉、面包和一些菜单项。
你准备好和我一起创建它了吗?
以下是我们将要制作的内容
查看 Maks Akymenko (@maximakymenko) 在 CodePen 上发布的 Pen
使用 React Hooks 和 Styled Components 制作的汉堡菜单
在 CodePen 上。
我们将在本教程中使用 React 进行构建,因为它似乎是一个很好的用例:我们获得了一个可重用的组件和一组可以扩展的 Hooks 来处理点击功能。
启动一个新的 React 项目
让我们使用 create-react-app 启动一个新项目,切换到该文件夹目录并添加 styled-components 来为 UI 设置样式
npx create-react-app your-project-name
cd your-project-name
yarn add styled-components
添加基本样式
在您最喜欢的代码编辑器中打开新创建的项目,并开始使用 styled-components 添加基本样式。在您的 src
目录中,创建一个名为 global.js
的文件。它将包含整个应用程序的样式。您可以编写自己的样式,或者复制我最终使用的样式
// global.js
import { createGlobalStyle } from 'styled-components';
export const GlobalStyles = createGlobalStyle`
html, body {
margin: 0;
padding: 0;
}
*, *::after, *::before {
box-sizing: border-box;
}
body {
align-items: center;
background: #0D0C1D;
color: #EFFFFA;
display: flex;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
height: 100vh;
justify-content: center;
text-rendering: optimizeLegibility;
}
`
这只是全局样式的一部分,其余部分可以在 此处 找到。
CreateGlobalStyle
函数通常用于创建全局样式,这些样式会暴露给整个应用程序。我们将导入它,以便在需要时可以访问这些样式。
下一步是添加一个包含所有变量的主题文件。在 src
目录中创建一个 theme.js
文件并添加以下内容
// theme.js
export const theme = {
primaryDark: '#0D0C1D',
primaryLight: '#EFFFFA',
primaryHover: '#343078',
mobile: '576px',
}
添加布局、菜单和汉堡组件 🍔
转到您的 App.js
文件。我们将清空其中的所有内容,并为我们的应用程序创建主模板。以下是我所做的。您当然可以创建自己的模板。
// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
function App() {
return (
<ThemeProvider theme={theme}>
<>
<GlobalStyles />
<div>
<h1>Hello. This is burger menu tutorial</h1>
<img src="https://image.flaticon.com/icons/svg/2016/2016012.svg" alt="burger icon" />
<small>Icon made by Freepik from www.flaticon.com</small>
</div>
</>
</ThemeProvider>
);
}
export default App;
不要忘记添加包含 small
标签的行。这是我们感谢 flaticon.comhttp://flaticon.com) 作者提供的图标的方式。
到目前为止,我们已经完成了以下工作

让我稍微解释一下。我们导入了 ThemeProvider
,它是一个包装组件,在幕后使用 Context API 使我们的 theme
变量可用于整个组件树。
我们还导入了 GlobalStyles
并将其作为组件传递给我们的应用程序,这意味着我们的应用程序现在可以访问所有全局样式。如您所见,我们的 GlobalStyles
组件位于 ThemeProvider
内部,这意味着我们已经可以对其进行一些小的更改。
转到 global.js
并更改 background
和 color
属性以使用我们定义的变量。这有助于我们实现主题,而不是使用难以更改的固定值。
// global.js
background: ${({ theme }) => theme.primaryDark};
color: ${({ theme }) => theme.primaryLight};
我们从 props
中解构 theme
。因此,我们使用了一堆 **方括号** 而不是每次都编写 props.theme
。我再说一遍:theme
可用是因为我们将全局样式用 ThemeProvider
包裹了起来。
创建汉堡和菜单组件
在 src
目录内创建一个 components
文件夹,并在其中添加两个文件夹:Menu
和 Burger
,以及一个 index.js
文件。

index.js
将用于一个目的:允许我们从一个文件中导入组件,这非常方便,尤其是在您有很多组件时。
现在让我们创建我们的组件。每个文件夹将包含三个文件。
所有这些文件有什么作用?您很快就会看到可扩展结构的好处。它在我的几个项目中运行良好,但这里有一个很好的建议 如何创建可扩展结构。
转到 Burger
文件夹,为我们的布局创建 Burger.js
。然后添加 Burger.styled.js
,它将包含样式,以及 index.js
,它将导出该文件。
// index.js
export { default } from './Burger';
随意以您想要的方式设置汉堡切换按钮的样式,或者只粘贴这些样式
// Burger.styled.js
import styled from 'styled-components';
export const StyledBurger = styled.button`
position: absolute;
top: 5%;
left: 2rem;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 2rem;
height: 2rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
z-index: 10;
&:focus {
outline: none;
}
div {
width: 2rem;
height: 0.25rem;
background: ${({ theme }) => theme.primaryLight};
border-radius: 10px;
transition: all 0.3s linear;
position: relative;
transform-origin: 1px;
}
`;
transform-origin
属性将在稍后用于为菜单在打开和关闭状态之间切换时设置动画。
添加样式后,转到 Burger.js
并添加布局
// Burger.js
import React from 'react';
import { StyledBurger } from './Burger.styled';
const Burger = () => {
return (
<StyledBurger>
<div />
<div />
<div />
</StyledBurger>
)
}
export default Burger;
之后查看左上角。你看到了吗?
现在对 Menu
文件夹执行相同的操作
// Menu -> index.js
export { default } from './Menu';
// Menu.styled.js
import styled from 'styled-components';
export const StyledMenu = styled.nav`
display: flex;
flex-direction: column;
justify-content: center;
background: ${({ theme }) => theme.primaryLight};
height: 100vh;
text-align: left;
padding: 2rem;
position: absolute;
top: 0;
left: 0;
transition: transform 0.3s ease-in-out;
@media (max-width: ${({ theme }) => theme.mobile}) {
width: 100%;
}
a {
font-size: 2rem;
text-transform: uppercase;
padding: 2rem 0;
font-weight: bold;
letter-spacing: 0.5rem;
color: ${({ theme }) => theme.primaryDark};
text-decoration: none;
transition: color 0.3s linear;
@media (max-width: ${({ theme }) => theme.mobile}) {
font-size: 1.5rem;
text-align: center;
}
&:hover {
color: ${({ theme }) => theme.primaryHover};
}
}
`;
接下来,让我们为点击汉堡菜单时显示的菜单项添加布局
// Menu.js
import React from 'react';
import { StyledMenu } from './Menu.styled';
const Menu = () => {
return (
<StyledMenu>
<a href="/">
<span role="img" aria-label="about us">💁🏻‍♂️</span>
About us
</a>
<a href="/">
<span role="img" aria-label="price">💸</span>
Pricing
</a>
<a href="/">
<span role="img" aria-label="contact">📩</span>
Contact
</a>
</StyledMenu>
)
}
export default Menu;
我们这里有一些不错的表情符号,最佳实践是通过将每个表情符号包装在一个 span 中并添加几个属性来使其可访问:role="img"
和 aria-label="your label"
。您可以在 此处 阅读更多相关信息。
现在将我们的新组件导入到 App.js
文件中
// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';
// ...
让我们看看我们得到了什么

看看这个漂亮的导航栏!但是我们这里有一个问题:它是打开的,而我们希望它最初是关闭的。我们只需要在 Menu.styled.js
中添加一行即可解决它
// Menu.styled.js
transform: translateX(-100%);
我们正在努力将这个汉堡制作完成!但首先…
添加打开和关闭功能
我们希望在点击汉堡图标时打开侧边栏,所以让我们开始吧。打开 App.js
并向其中添加一些状态。我们将使用 useState Hook 来实现。
// App.js
import React, { useState } from 'react';
导入它后,让我们在 App
组件内部使用它。
// App.js
const [open, setOpen] = useState(false);
我们将初始状态设置为 false
,因为在渲染应用程序时我们的菜单应该隐藏。
我们的切换按钮和侧边栏菜单都需要了解状态,因此将其作为 prop 传递给每个组件。现在您的 App.js
应该如下所示
// App.js
import React, { useState } from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';
function App() {
const [open, setOpen] = useState(false);
return (
<ThemeProvider theme={theme}>
<>
<GlobalStyles />
<div>
<h1>Hello. This is burger menu tutorial</h1>
<img src="https://media.giphy.com/media/xTiTnwj1LUAw0RAfiU/giphy.gif" alt="animated burger" />
</div>
<div>
<Burger open={open} setOpen={setOpen} />
<Menu open={open} setOpen={setOpen} />
</div>
</>
</ThemeProvider>
);
}
export default App;
请注意,我们正在将组件包装在一个 div 中。这在稍后添加点击屏幕任何位置关闭菜单的功能时将很有帮助。
处理组件中的 props
我们的 Burger
和 Menu
知道状态,所以我们只需要在内部处理它并相应地添加样式即可。转到 Burger.js
并处理传递下来的 props
// Burger.js
import React from 'react';
import { bool, func } from 'prop-types';
import { StyledBurger } from './Burger.styled';
const Burger = ({ open, setOpen }) => {
return (
<StyledBurger open={open} onClick={() => setOpen(!open)}>
<div />
<div />
<div />
</StyledBurger>
)
}
Burger.propTypes = {
open: bool.isRequired,
setOpen: func.isRequired,
};
export default Burger;
我们将 open
和 setOpen
属性解构,并将它们分别传递给我们的 StyledBurger
以添加每个属性的样式。此外,我们添加了 onClick
处理程序来调用我们的 setOpen
函数并切换 open
属性。在文件末尾,我们添加了类型检查,这被认为是将参数与预期数据对齐的最佳实践。
您可以通过转到您的react-dev-tools来检查它是否有效。在您的 Chrome DevTools 中转到“组件”选项卡,然后单击“汉堡包”选项卡。

现在,当您单击“汉堡包”组件时(不要与选项卡混淆),您应该会看到 open
复选框正在更改其状态。
转到 Menu.js
并执行几乎相同的操作,尽管在这里我们只传递 open
属性。
// Menu.js
import React from 'react';
import { bool } from 'prop-types';
import { StyledMenu } from './Menu.styled';
const Menu = ({ open }) => {
return (
<StyledMenu open={open}>
<a href="/">
<span role="img" aria-label="about us">💁🏻‍♂️</span>
About us
</a>
<a href="/">
<span role="img" aria-label="price">💸</span>
Pricing
</a>
<a href="/">
<span role="img" aria-label="contact">📩</span>
Contact
</a>
</StyledMenu>
)
}
Menu.propTypes = {
open: bool.isRequired,
}
export default Menu;
下一步是将 open
属性传递给我们的样式化组件,以便我们可以应用过渡。打开 Menu.styled.js
并将以下内容添加到我们的 transform
属性中。
transform: ${({ open }) => open ? 'translateX(0)' : 'translateX(-100%)'};
这将检查我们的样式化组件 open
属性是否为 true
,如果是,则添加 translateX(0)
以将我们的导航移回屏幕上。您已经可以在本地测试它了!

等等!
您在检查时有没有注意到什么问题?我们的“汉堡包”与“菜单”的背景颜色相同,这使得它们融合在一起。让我们更改它,并稍微动画化图标以使其更有趣。我们已经将 open
属性传递给了它,因此我们可以使用它来应用更改。
打开 Burger.styled.js
并编写以下内容。
// Burger.styled.js
import styled from 'styled-components';
export const StyledBurger = styled.button`
position: absolute;
top: 5%;
left: 2rem;
display: flex;
flex-direction: column;
justify-content: space-around;
width: 2rem;
height: 2rem;
background: transparent;
border: none;
cursor: pointer;
padding: 0;
z-index: 10;
&:focus {
outline: none;
}
div {
width: 2rem;
height: 0.25rem;
background: ${({ theme, open }) => open ? theme.primaryDark : theme.primaryLight};
border-radius: 10px;
transition: all 0.3s linear;
position: relative;
transform-origin: 1px;
:first-child {
transform: ${({ open }) => open ? 'rotate(45deg)' : 'rotate(0)'};
}
:nth-child(2) {
opacity: ${({ open }) => open ? '0' : '1'};
transform: ${({ open }) => open ? 'translateX(20px)' : 'translateX(0)'};
}
:nth-child(3) {
transform: ${({ open }) => open ? 'rotate(-45deg)' : 'rotate(0)'};
}
}
`;
这是一大块 CSS 代码,但它实现了动画效果。我们检查 open
属性是否为 true
并相应地更改样式。我们旋转、平移,然后隐藏菜单图标的线条,同时更改颜色。很漂亮,不是吗?

好了,朋友们!到目前为止,您应该知道如何创建一个简单的汉堡包图标和菜单,其中包含响应性和流畅的动画。恭喜!
但还有一件事我们应该考虑……
通过单击菜单外部关闭菜单
这部分看起来像是一个小奖励,但它是一个重大的 UX 提升,因为它允许用户通过单击页面上的任何其他位置来关闭菜单。这有助于用户避免必须重新定位菜单图标并精确点击它。
我们将使用更多 React hook 来实现这一点!在 src
目录中创建一个名为 hooks.js
的文件并打开它。对于这个,我们将转向在 React 18 中引入的 useEffect
hook。
// hooks.js
import { useEffect } from 'react';
在编写代码之前,让我们考虑一下此 hook 背后的逻辑。当我们在页面上的某个位置单击时,我们需要检查单击的元素是否是我们的当前元素(在我们的例子中,它是 Menu
组件),或者单击的元素是否包含当前元素(例如,我们的 div 包含我们的菜单和汉堡包图标)。如果是,我们什么也不做,否则,我们调用一个函数,我们将其命名为 handler
。
我们将使用 ref
来检查单击的元素,并且每次有人在页面上单击时都会这样做。
// hooks.js
import { useEffect } from 'react';
export const useOnClickOutside = (ref, handler) => {
useEffect(() => {
const listener = event => {
if (!ref.current || ref.current.contains(event.target)) {
return;
}
handler(event);
};
document.addEventListener('mousedown', listener);
return () => {
document.removeEventListener('mousedown', listener);
};
},
[ref, handler],
);
};
不要忘记从 useEffect
返回函数。这就是所谓的“清理”,基本上,它代表在组件卸载时删除事件监听器。它是 componentWillUnmount
生命周期方法的替代。
现在让我们连接 hook
我们的 hook 已准备就绪,因此是时候将其添加到应用程序中了。转到 App.js
文件,并导入两个 hook:新创建的 useOnClickOutside
以及 useRef
。我们将需要后者来获取对元素的引用。
// App.js
import React, { useState, useRef } from 'react';
import { useOnClickOutside } from './hooks';
为了在当前元素中访问这些内容,我们需要访问 DOM 节点。这就是我们使用 useRef
的地方,此外,名称 node
完美地反映了此变量的用途。
从那里,我们将 node
作为第一个参数传递。我们将传递关闭菜单的函数作为第二个参数。
// App.js
const node = useRef();
useOnClickOutside(node, () => setOpen(false));
最后,我们需要将我们的 ref
传递给 DOM 元素。在我们的例子中,它将是包含 Burger
和 Menu
组件的 div
。
// App.js
<div ref={node}>
<Burger open={open} setOpen={setOpen} />
<Menu open={open} setOpen={setOpen} />
</div>
您的 App.js
应该类似于此。
// App.js
import React, { useState, useRef } from 'react';
import { ThemeProvider } from 'styled-components';
import { useOnClickOutside } from './hooks';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';
function App() {
const [open, setOpen] = useState(false);
const node = useRef();
useOnClickOutside(node, () => setOpen(false));
return (
<ThemeProvider theme={theme}>
<>
<GlobalStyles />
<div>
<h1>Hello. This is burger menu tutorial</h1>
<img src="https://media.giphy.com/media/xTiTnwj1LUAw0RAfiU/giphy.gif" alt="animated burger" />
</div>
<div ref={node}>
<Burger open={open} setOpen={setOpen} />
<Menu open={open} setOpen={setOpen} />
</div>
</>
</ThemeProvider>
);
}
export default App;
看看!它按预期工作,并且功能齐全且具有响应性。

恭喜大家!你们做得很好!祝您编码愉快!
感谢此文章。一直很难找到适合我项目的动画资源,现在我找到了!React Reveal 太棒了。
-约瑟夫
很棒的文章!删除这些样式化组件会注入一个巨大的包,有时比您的应用程序更大。这项技术很棒。
嗨,马克斯,文章中缺少一部分指南。
组件 > index.js,
并且这部分,transform 值不应包含引号。
嗨!感谢您提到这一点。
这句话描述了它:
index.js 将用于一个目的:允许我们从一个文件中导入组件,这非常方便,尤其是在您有很多组件时。
很好地捕捉到了,安森,但是代码不应该是
export {Burger as default} from "./Burger/Burger"
如果错误,请更正。
所以我想知道,在这种情况下哪一个是正确的?
嗨,马克斯,
非常有益的文章。但在可访问性方面,您错过了三件事。
您的代码示例中,汉堡包按钮和关闭按钮都没有可访问的名称。
即使侧边栏导航不可见,您也可以通过键盘访问它。
侧边栏导航打开时,键盘(以及屏幕阅读器)焦点不会被捕获在其中。
在我看来,教程,尤其是像 css-tricks 这样的热门网站上的教程应该是可访问的(因为人们倾向于复制和粘贴教程代码)。如果您对此有任何疑问,请与我联系。
最好的祝愿
马库斯
嗨,马库斯!您提到了非常重要的事情!我在发布文章后,有人提交了一个 PR 并添加了可访问性功能。我在 repo 中留下了关于此的注释。
嗨,马库斯,
我完全同意,并且想过要发表评论,但相反,我向 GitHub 存储库提交了一个 PR(如马克斯提到的)——在我的代码中,我修复了您提到的的大多数问题以及其他问题。您可以在此处查看 PR:https://github.com/maximakymenko/react-burger-menu-article-app/pull/2
我未修复的一件事是在导航中捕获焦点,并且我不确定最好的方法是什么,尤其是在我的 React 技能仍然处于较低水平的情况下。也许您或其他读者可以在这里提供帮助?
最好的祝愿
金
嗨,金,
感谢您宝贵的 PR!
关于焦点捕获——我也不是 React 专家,但这应该有效:https://github.com/maximakymenko/react-burger-menu-article-app/pull/3
此致
马库斯
嗨,
当我创建应用程序时,它没有添加样式化组件。应用程序创建后,如何添加样式化组件?
一个抱歉的新手
嗨,詹姆斯!
从应用程序文件夹打开您的终端并编写以下内容。
或者如果您使用
NPM
你可以直接使用‘yarn add styled-components’ 或 ‘npm add styled-components’
很棒的文章。
我认为如果这些教程中的代码示例能够显示文件的完整路径来指示代码在项目结构中的位置,则可以得到改进。我猜目前作者必须手动在代码中键入文件目录作为注释,例如
//index.js
但完整路径会更有帮助,例如
// src/components/index.js
或
// src/components/Burger/index.js
是否有办法在 CMS 中实现这一点?可能在待办事项列表中 :)
这会让我们(我)这些还没有完全掌握导入和导出的人更容易一些
谢谢
嗨,James!感谢你的提醒。我可能会考虑在我的下一篇文章中添加完整路径。另外,如果你在实现或其他方面有任何问题,请随时联系我,我可能会帮助你!
本教程缺少一些使所有内容都能正常工作的关键部分。我猜这是一个很好的学习体验。
嗨,马克斯,
很棒的文章!只是好奇,如果你只想要在移动设备上显示汉堡菜单,而在移动宽度以上则将其显示在顶部的标题菜单中,最佳方法是什么。实现起来简单吗?
谢谢!
CB
你好!感谢你的提问!
我会使用 CSS(Styled-components)和媒体查询来实现。我会在顶部设置导航样式并隐藏汉堡菜单,然后在 CSS 中设置断点(例如 768px),这样当屏幕变小时,就可以相应地更改样式(例如显示汉堡菜单,更改导航位置,重新设置样式)。
如何修改它以便轻松添加到其他项目中?
这个演练对我理解我正在编写的 React 代码非常有帮助。
你能解释一下你在自定义 useEffect 钩子中的这段代码吗?
if (!ref.current ||
ref.current.contains(event.target)) {
return;}
我理解为什么 (ref.current.contains(event.target)) 会返回,但我不知道为什么 (!ref.current) 也是必要的代码?再次感谢!
通过返回 removeEventListener 来清理 useOnClickOutside 钩子的目的是什么?
这个 hooks.js 组件始终在监听 mousedown。
有什么原因吗?