Hamburger-Menü mit einer Beilage von React Hooks und Styled Components

Avatar of Maks Akymenko
Maks Akymenko am

DigitalOcean bietet Cloud-Produkte für jede Phase Ihrer Reise. Starten Sie mit 200 $ kostenlosem Guthaben!

Wir alle wissen, was ein Hamburger-Menü ist, oder? Als das Muster seinen Weg in das Webdesign fand, wurde es sowohl verspottet als auch bejubelt für seine Minimalismus, der es ermöglicht, Hauptmenüs vom Bildschirm auszublenden, insbesondere auf Mobilgeräten, wo jeder Pixel Platz zählt.

CSS-Tricks ist all about double the meat.

Ob man sie liebt oder hasst, Hamburger-Menüs sind da und werden wahrscheinlich noch eine Weile bleiben. Das Problem ist, wie man sie implementiert. Sicher, sie sehen einfach und unkompliziert aus, aber das können sie durchaus sein. Sollten sie zum Beispiel mit einem Label kombiniert werden? Sind sie auf der linken oder rechten Seite des Bildschirms effektiver? Wie gehen wir damit um, diese Menüs zu schließen, sei es durch Klicken oder Berühren? Sollte das Icon ein SVG, eine Schriftart, ein Unicode-Zeichen oder reines CSS sein? Wie wäre es mit einer fleischlosen Option?

Ich wollte eines davon bauen, fand aber keine einfache Lösung. Die meisten Lösungen basieren auf Bibliotheken wie reactjs-popup oder react-burger-menu. Diese sind großartig, aber für komplexere Lösungen. Was ist mit dem Kernanwendungsfall eines Drei-Linien-Menüs, das einfach ein Panel von der Seite des Bildschirms ausfährt, wenn es angeklickt wird, und dann das Panel wieder einfährt, wenn es erneut angeklickt wird?

Ich beschloss, mein eigenes einfaches Hamburger-Menü mit Seitenleiste zu bauen. Keine Essiggurken, Zwiebeln oder Ketchup. Nur Fleisch, Brötchen und eine Beilage von Menüpunkten.

Sind Sie bereit, es mit mir zu erstellen?

Hier ist, was wir machen

Sieh dir den Pen
Burger-Menü mit React Hooks und styled-components
von Maks Akymenko (@maximakymenko) an
auf CodePen.

Wir bauen diese Anleitung mit React, weil es wie ein guter Anwendungsfall dafür erscheint: Wir erhalten eine wiederverwendbare Komponente und einen Satz von Hooks, die wir erweitern können, um die Klickfunktionalität zu handhaben.

Ein neues React-Projekt aufsetzen

Lassen Sie uns ein neues Projekt mit create-react-app aufsetzen, in diesen Ordner wechseln und styled-components hinzufügen, um die Benutzeroberfläche zu gestalten.

npx create-react-app your-project-name
cd your-project-name
yarn add styled-components

Grundlegende Stile hinzufügen

Öffnen Sie das neu erstellte Projekt in Ihrem bevorzugten Code-Editor und beginnen Sie mit dem Hinzufügen von grundlegenden Stilen mit styled-components. Erstellen Sie in Ihrem src-Verzeichnis eine Datei namens global.js. Sie wird Stile für die gesamte App enthalten. Sie können Ihre eigenen schreiben oder einfach kopieren, was ich am Ende getan habe.

// 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;
  }
  `

Dies ist nur ein Teil der globalen Stile, den Rest finden Sie hier.

Die Funktion CreateGlobalStyle wird im Allgemeinen verwendet, um globale Stile zu erstellen, die für die gesamte App verfügbar sind. Wir werden sie importieren, damit wir während der Arbeit Zugriff auf diese Stile haben.

Der nächste Schritt ist das Hinzufügen einer Theme-Datei, die alle unsere Variablen enthält. Erstellen Sie eine Datei theme.js im src-Verzeichnis und fügen Sie Folgendes hinzu.

// theme.js
export const theme = {
  primaryDark: '#0D0C1D',
  primaryLight: '#EFFFFA',
  primaryHover: '#343078',
  mobile: '576px',
}

Layout-, Menü- und Hamburger-Komponenten hinzufügen 🍔

Gehen Sie zu Ihrer Datei App.js. Wir werden alles daraus löschen und die Hauptvorlage für unsere App erstellen. Hier ist, was ich getan habe. Sie können sicherlich Ihre eigene erstellen.

// 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;

Vergessen Sie nicht, die Zeile mit dem small-Tag hinzuzufügen. So weisen wir den Autoren von flaticon.comhttp://flaticon.com) für das bereitgestellte Icon die Anerkennung zu.

Hier ist, was wir bisher haben.

Ich erkläre ein wenig. Wir haben ThemeProvider importiert, eine Wrapper-Komponente, die die Context API im Hintergrund verwendet, um unsere theme-Variablen für den gesamten Komponentenbaum verfügbar zu machen.

Wir haben auch unsere GlobalStyles importiert und sie als Komponente an unsere App übergeben, was bedeutet, dass unsere Anwendung nun Zugriff auf alle globalen Stile hat. Wie Sie sehen können, befindet sich unsere GlobalStyles-Komponente innerhalb von ThemeProvider, was bedeutet, dass wir bereits einige kleinere Änderungen daran vornehmen können.

Gehen Sie zu global.js und ändern Sie die Eigenschaften background und color, um unsere definierten Variablen zu verwenden. Dies hilft uns, ein Theme zu implementieren, anstatt feste Werte zu verwenden, die schwer zu ändern sind.

// global.js
background: ${({ theme }) => theme.primaryDark};
color: ${({ theme }) => theme.primaryLight};

Wir destrukturieren unser theme aus props. Anstatt also jedes Mal props.theme zu schreiben, verwenden wir eine Reihe von Klammern. Ich wiederhole mich: Das theme ist verfügbar, weil wir unsere globalen Stile mit ThemeProvider umschlossen haben.

Burger- und Menükomponenten erstellen

Erstellen Sie einen Ordner components im src-Verzeichnis und fügen Sie darin zwei Ordner hinzu: Menu und Burger, sowie eine Datei index.js.

index.js wird für einen einzigen Zweck verwendet: um uns zu ermöglichen, Komponenten aus einer Datei zu importieren, was sehr praktisch ist, besonders wenn man viele davon hat.

Nun erstellen wir unsere Komponenten. Jeder Ordner wird drei Dateien enthalten.

Was ist mit all den Dateien los? Sie werden den Vorteil einer skalierbaren Struktur bald genug sehen. Sie hat für mich in einigen Projekten gut funktioniert, aber hier ist ein guter Rat wie man eine skalierbare Struktur erstellt.

Gehen Sie zum Ordner Burger und erstellen Sie Burger.js für unser Layout. Fügen Sie dann Burger.styled.js hinzu, das die Stile enthalten wird, und index.js, das die Datei exportieren wird.

// index.js
export { default } from './Burger';

Sie können den Burger-Toggle beliebig gestalten oder diese Stile einfach einfügen.

// 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;
  }
`;

Die Eigenschaft transform-origin wird später benötigt, um das Menü zu animieren, wenn es zwischen offenem und geschlossenem Zustand wechselt.

Nachdem Sie die Stile hinzugefügt haben, gehen Sie zu Burger.js und fügen Sie das Layout hinzu.

// Burger.js
import React from 'react';
import { StyledBurger } from './Burger.styled';

const Burger = () => {
  return (
    <StyledBurger>
      <div />
      <div />
      <div />
    </StyledBurger>
  )
}

export default Burger;

Schauen Sie dann in die obere linke Ecke. Sehen Sie es?

Zeit, dasselbe mit dem Menu-Ordner zu tun.

// 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};
    }
  }
`;

Als Nächstes fügen wir das Layout für die Menüpunkte hinzu, die beim Klicken auf unseren Burger angezeigt werden.

// Menu.js
import React from 'react';
import { StyledMenu } from './Menu.styled';

const Menu = () => {
  return (
    <StyledMenu>
      <a href="/">
        <span role="img" aria-label="about us">&#x1f481;&#x1f3fb;&#x200d;&#x2642;&#xfe0f;</span>
        About us
      </a>
      <a href="/">
        <span role="img" aria-label="price">&#x1f4b8;</span>
        Pricing
        </a>
      <a href="/">
        <span role="img" aria-label="contact">&#x1f4e9;</span>
        Contact
        </a>
    </StyledMenu>
  )
}
export default Menu;

Wir haben hier schöne Emojis, und Best Practice ist, sie zugänglich zu machen, indem wir jeden in ein Span einpacken und ein paar Eigenschaften hinzufügen: role="img" und aria-label="ihr Label". Sie können mehr darüber hier lesen.

Zeit, unsere neuen Komponenten in unsere Datei App.js zu importieren.

// App.js
import React from 'react';
import { ThemeProvider } from 'styled-components';
import { GlobalStyles } from './global';
import { theme } from './theme';
import { Burger, Menu } from './components';

// ...

Sehen wir mal, was wir haben.

Schauen Sie sich diese schöne Navigationsleiste an! Aber wir haben hier ein Problem: Sie ist geöffnet, und wir wollen, dass sie anfangs geschlossen ist. Wir müssen nur eine Zeile zu Menu.styled.js hinzufügen, um das zu beheben.

// Menu.styled.js
transform: translateX(-100%);

Wir sind auf dem besten Weg, diesen Burger als "gekocht" zu bezeichnen! Aber zuerst...

Funktionalität zum Öffnen und Schließen hinzufügen

Wir wollen die Seitenleiste öffnen, wenn auf das Hamburger-Icon geklickt wird. Gehen wir also daran. Öffnen Sie App.js und fügen Sie dort etwas State hinzu. Wir werden den useState-Hook dafür verwenden.

// App.js
import React, { useState } from 'react';

Nachdem Sie ihn importiert haben, verwenden wir ihn in der App-Komponente.

// App.js
const [open, setOpen] = useState(false);

Wir setzen den anfänglichen State auf false, da unser Menü versteckt sein soll, wenn die Anwendung gerendert wird.

Wir benötigen sowohl unseren Toggle als auch das Seitenmenü, um über den State Bescheid zu wissen. Übergeben Sie ihn also als Prop an jede Komponente. Jetzt sollte Ihre App.js etwa so aussehen.

// 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;

Beachten Sie, dass wir unsere Komponenten in einem div umschließen. Das wird später hilfreich sein, wenn wir Funktionalität hinzufügen, die das Menü schließt, wenn irgendwo auf dem Bildschirm geklickt wird.

Props in den Komponenten behandeln

Unser Burger und Menu kennen den State, also müssen wir ihn nur noch intern behandeln und die Stile entsprechend anpassen. Gehen Sie zu Burger.js und behandeln Sie die übergebenen 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;

Wir destrukturieren die Props open und setOpen und übergeben sie an unseren StyledBurger, um Stile für jede Prop entsprechend hinzuzufügen. Außerdem fügen wir den onClick-Handler hinzu, um unsere setOpen-Funktion aufzurufen und die open-Prop umzuschalten. Am Ende der Datei fügen wir Typüberprüfung hinzu, die als Best Practice gilt, um Argumente mit den erwarteten Daten abzugleichen.

Sie können überprüfen, ob es funktioniert oder nicht, indem Sie zu Ihren React-Entwicklertools gehen. Gehen Sie zum Tab Components in Ihren Chrome DevTools und klicken Sie auf den Tab Burger.

Wenn Sie nun auf Ihre Burger-Komponente klicken (nicht mit dem Tab verwechseln), sollten Sie sehen, dass Ihre open-Checkbox ihren Zustand ändert.

Gehen Sie zu Menu.js und machen Sie fast dasselbe, obwohl wir hier nur die open-Prop übergeben.

// 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">&#x1f481;&#x1f3fb;&#x200d;&#x2642;&#xfe0f;</span>
        About us
      </a>
      <a href="/">
        <span role="img" aria-label="price">&#x1f4b8;</span>
        Pricing
        </a>
      <a href="/">
        <span role="img" aria-label="contact">&#x1f4e9;</span>
        Contact
        </a>
    </StyledMenu>
  )
}
Menu.propTypes = {
  open: bool.isRequired,
}
export default Menu;

Der nächste Schritt ist die Übergabe der open-Prop an unsere Styled Component, damit wir den Übergang anwenden können. Öffnen Sie Menu.styled.js und fügen Sie Folgendes zu unserer transform-Eigenschaft hinzu.

transform: ${({ open }) => open ? 'translateX(0)' : 'translateX(-100%)'};

Dies prüft, ob unsere Styled Component open Prop true ist, und fügt, falls ja, translateX(0) hinzu, um unsere Navigation zurück auf den Bildschirm zu bewegen. Sie können das bereits lokal testen!

Moment, Moment, Moment!

Haben Sie beim Ausprobieren etwas Falsches bemerkt? Unser Burger hat die gleiche Farbe wie die Hintergrundfarbe unseres Menu, was dazu führt, dass sie miteinander verschmelzen. Lassen Sie uns das ändern und auch die Animation des Icons etwas interessanter gestalten. Wir haben die open-Prop an sie übergeben, also können wir sie verwenden, um die Änderungen anzuwenden.

Öffnen Sie Burger.styled.js und schreiben Sie Folgendes.

// 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)'};
    }
  }
`;

Das ist ein großer Block CSS, aber er lässt die Animationsmagie geschehen. Wir prüfen, ob die open-Prop true ist und ändern die Stile entsprechend. Wir drehen, verschieben und verstecken dann die Linien des Menü-Icons, während wir die Farbe ändern. Schön, nicht wahr?

Okay, Leute! Bis dahin sollten Sie wissen, wie man ein einfaches Hamburger-Icon und Menü erstellt, das Responsivität und flüssige Animationen beinhaltet. Herzlichen Glückwunsch!

Aber es gibt noch eine letzte Sache, um die wir uns kümmern müssen...

Menü durch Klicken außerhalb schließen

Dieser Teil scheint ein kleiner Bonus zu sein, aber er ist ein großer UX-Gewinn, da er dem Benutzer ermöglicht, das Menü durch Klicken an einer beliebigen anderen Stelle auf der Seite zu schließen. Dies hilft dem Benutzer, die Menü-Ikone nicht erneut suchen und genau auf sie klicken zu müssen.

Wir werden weitere React-Hooks verwenden, um dies zu ermöglichen! Erstellen Sie eine Datei im src-Verzeichnis namens hooks.js und öffnen Sie sie. Für diese werden wir uns dem useEffect-Hook zuwenden, der in React 18 eingeführt wurde.

// hooks.js
import { useEffect } from 'react';

Bevor wir den Code schreiben, denken wir über die Logik hinter diesem Hook nach. Wenn wir irgendwo auf der Seite klicken, müssen wir prüfen, ob das geklickte Element unser aktuelles Element ist (in unserem Fall die Menu-Komponente) oder ob das geklickte Element das aktuelle Element enthält (zum Beispiel unser div, das unser Menü und die Hamburger-Ikone umschließt). Wenn ja, tun wir nichts, andernfalls rufen wir eine Funktion auf, die wir handler nennen.

Wir werden ref verwenden, um das geklickte Element zu überprüfen, und wir werden dies jedes Mal tun, wenn jemand auf die Seite klickt.

// 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],
  );
};

Vergessen Sie nicht, die Funktion aus useEffect zurückzugeben. Das ist die sogenannte "Bereinigung" und steht im Grunde für das Entfernen eines Event-Listeners, wenn die Komponente unmontiert wird. Es ist der Ersatz für den Lifecycle-Hook componentWillUnmount.

Nun wollen wir den Hook einbinden

Wir haben unseren Hook bereit, also ist es Zeit, ihn zur App hinzuzufügen. Gehen Sie zur Datei App.js und importieren Sie zwei Hooks: den neu erstellten useOnClickOutside und auch useRef. Letzteren werden wir benötigen, um eine Referenz auf das Element zu erhalten.

// App.js
import React, { useState, useRef } from 'react';
import { useOnClickOutside } from './hooks';

Um auf diese im aktuellen Element zuzugreifen, müssen wir auf den DOM-Knoten zugreifen. Dort verwenden wir useRef, und der Name node spiegelt den Zweck dieser Variable perfekt wider.

Von dort übergeben wir den node als erstes Argument. Die Funktion, die unser Menü schließt, übergeben wir als zweites Argument.

// App.js
const node = useRef(); 
useOnClickOutside(node, () => setOpen(false));

Zuletzt müssen wir unseren ref an das DOM-Element übergeben. In unserem Fall wird es das div sein, das die Komponenten Burger und Menu enthält.

// App.js
<div ref={node}>
  <Burger open={open} setOpen={setOpen} />
  <Menu open={open} setOpen={setOpen} />
</div>

Ihre App.js sollte in etwa so aussehen.

// 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;

Schauen Sie sich das an! Es funktioniert wie erwartet und ist voll funktionsfähig und reaktionsschnell.

Herzlichen Glückwunsch an alle! Sie haben großartige Arbeit geleistet! Viel Spaß beim Codieren!

Auf GitHub ansehen