reactjsreact-iconsreact-scroll

Change navbar active class on click and scroll using React


I was trying to do a floating sticky nav bar in React using simple HTML and CSS and some JS. Clicking on the button will take me to the page but it won't change the navbar active class sometimes and also i want the nav bar active class to change as I scroll. I tried scroll-spy react-scroll but its not supporting my nav styling.

I couldn't find a perfect solution anywhere on the internet. I was trying to do my portfolio but got stuck at the first part itself.

import React from 'react'
import './navbar.css'
import {AiFillHome} from 'react-icons/ai'
import {GiBrain} from 'react-icons/gi'
import {BsTools} from 'react-icons/bs'
import {IoCall} from 'react-icons/io5'
import {IoMdContact} from 'react-icons/io'
// import {Link} from 'react-scroll'
// import { useState, useEffect } from "react"

function Navbar() {
  const list = document.querySelectorAll(".list");

function activeLink() {
  list.forEach((item) => item.classList.remove("active"));
  this.classList.add("active");
}
list.forEach((item) => item.addEventListener("click", activeLink));


return(
    <div class="navigation">
  <ul>
    <li class="list active">
      <a href="#home">
        <span class="icon">
        <AiFillHome />
        </span>
        <span class="text">Home</span>
      </a>
    </li>
    <li class="list">
      <a href="#skills">
        <span class="icon">
        <GiBrain />
        </span>
        <span class="text">Skills</span>
      </a>
    </li>
    <li class="list">
      <a href="#projects">
        <span class="icon">
        <BsTools/>
        </span>
        <span class="text">Projects</span>
      </a>
    </li>
    <li class="list">
      <a href="#about">
        <span class="icon">
        <IoMdContact/>
        </span>
        <span class="text">About </span>
      </a>
    </li>
    <li class="list">
      <a href="#contact">
        <span class="icon">
        <IoCall />
        </span>
        <span class="text">Contact</span>
      </a>
    </li>
    <div class="indicator"></div>
  </ul>
</div>
      
  )

}

export default Navbar

This is my CSS styling

@import url("https://fonts.googleapis.com/css?family=Poppins:100,200,300,400,500,600,700,800,900");

* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Poppins", sans-serif;
}
:root {
  --clr: black;
}
body {
  background: var(--clr);
}
.navigation {
  background-color: rgb(200, 242, 251);
    width: max-content;
    display: block;
    padding: 0.2rem 0.6rem;
    z-index: 2;
    position: fixed;
    left: 50%;
    left: 50%;
    transform: translateX(-50%);
    top: 4rem;
    display: flex;
    gap: 0.8rem;
    border-radius: 3rem;
   
}
.navigation ul {
  display: flex;
  width: 350px;
}
.navigation ul li {
  position: relative;
  list-style: none;
  width: 70px;
  height: 70px;
  z-index: 1;
}
.navigation ul li a {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  width: 100%;
  text-align: center;
  font-weight: 500;
}
.navigation ul li .icon {
  position: relative;
  display: block;
  line-height: 75px;
  font-size: 1.7em;
  text-align: center;
  transition: 0.5s;
  color:#04499d;
}
.navigation ul li.active a .icon {
  transform: translateY(-40px);
  font-size: 1.7em;
  color: rgb(116, 204, 236);
}
.navigation ul li a .text {
  position: absolute;
  color: black;
  font-weight: 600;
  font-size: 1.05em;
  letter-spacing: 0.05em;
  transition: 0.5s;
  opacity: 0;
  transform: translateY(20px);
}
.navigation ul li.active a .text {
  opacity: 1;
  transform: translateY(10px);
  font-size: 0.9em;
  color: black;
  font-weight: 600;
}
.indicator {
  position: absolute;
  top: -50%;
  width: 70px;
  height: 70px;
  background: #032a59;
  border-radius: 50%;
  border: 6px solid var(--clr);
  transition: 0.5s;
}
.indicator::before {
  content: "";
  position: absolute;
  top: 50%;
  left: -22px;
  width: 20px;
  height: 20px;
  background: transparent;
  border-top-right-radius: 20px;
  box-shadow: 1px -10px 0 0 black;
}
.indicator::after {
  content: "";
  position: absolute;
  top: 50%;
  right: -20px;
  width: 20px;
  height: 20px;
  background: transparent;
  border-top-left-radius: 20px;
  box-shadow: -1px -10px 0 0 black;
}


.navigation ul li:nth-child(1).active ~ .indicator {
  transform: translateX(calc(70px * 0));
}
.navigation ul li:nth-child(2).active ~ .indicator {
  transform: translateX(calc(70px * 1));
}
.navigation ul li:nth-child(3).active ~ .indicator {
  transform: translateX(calc(70px * 2));
}
.navigation ul li:nth-child(4).active ~ .indicator {
  transform: translateX(calc(70px * 3));
}
.navigation ul li:nth-child(5).active ~ .indicator {
  transform: translateX(calc(70px * 4));
}

Solution

  • You have a good start, just try and do things the "React" way when possible. In your case this means using refs, state and onClick handlers instead of querySelectorAll, addEventListener, etc.

    Here's a working sandbox example of your navbar you can learn from which is using React for your component.

    App.jsx

    import "./navbar.css";
    import React, { useEffect, useRef } from "react";
    import Navbar from "./Navbar";
    
    const divs = [
      {
        id: "Home",
        bgColor: "grey",
      },
      {
        id: "Skills",
        bgColor: "white",
      },
      {
        id: "Projects",
        bgColor: "skyblue",
      },
      {
        id: "About",
        bgColor: "lightgreen",
      },
      {
        id: "Contact",
        bgColor: "lightsalmon",
      },
    ];
    
    function App() {
      const observerRefs = useRef([]);
    
      return (
        <div>
          <Navbar observerRefs={observerRefs} />
          {divs.map((div, key) => {
            return (
              <div
                id={div.id.toLowerCase()}
                style={{ height: "420px", backgroundColor: div.bgColor }}
              >
                <h1 ref={(el) => (observerRefs.current[key] = el)}>{div.id}</h1>
              </div>
            );
          })}
        </div>
      );
    }
    
    export default App;
    

    Navbar.jsx

    import React, { useEffect, useRef, useState } from "react";
    import { AiFillHome } from "react-icons/ai";
    import { GiBrain } from "react-icons/gi";
    import { BsTools } from "react-icons/bs";
    import { IoCall } from "react-icons/io5";
    import { IoMdContact } from "react-icons/io";
    
    const items = [
      {
        text: "Home",
        icon: AiFillHome,
      },
      {
        text: "Skills",
        icon: GiBrain,
      },
      {
        text: "Projects",
        icon: BsTools,
      },
      {
        text: "About",
        icon: IoMdContact,
      },
      {
        text: "Contact",
        icon: IoCall,
      },
    ];
    
    function Navbar({ observerRefs }) {
      const [visibleKey, setVisibleKey] = useState(0);
      const observers = useRef([]);
    
      const onClick = (item, key) => {
        setVisibleKey(key);
      };
    
      const observerCallback = async (e, key) => {
        if (e.length && e[0].isIntersecting) {
          setVisibleKey(key);
        }
      };
    
      useEffect(() => {
        if (observerRefs.current?.length && observers.current) {
          Array.from(Array(10).keys()).forEach((_u, key) => {
            observers.current[key] = new IntersectionObserver((e) =>
              observerCallback(e, key)
            );
            if (observerRefs.current[key]) {
              observers.current[key].observe(observerRefs.current[key]);
            }
          });
        }
        return () =>
          observers.current?.forEach((observer) => observer?.current?.disconnect());
      }, [observerRefs, observers]);
    
      return (
        <>
          <div className="navigation">
            <ul>
              {items.map((item, key) => {
                return (
                  <li
                    name={item.text.toLowerCase()}
                    key={`item-${key}`}
                    className={`list${key === visibleKey ? " active" : ""}`}
                    onClick={() => onClick(item, key)}
                  >
                    <a href={`#${item.text.toLowerCase()}`}>
                      <span className="icon">{<item.icon />}</span>
                      <span className="text">{item.text}</span>
                    </a>
                  </li>
                );
              })}
              <div className="indicator"></div>
            </ul>
          </div>
        </>
      );
    }
    
    export default Navbar;