Search code examples
javascriptcssreactjsresizenavbar

How do I make and element stay with another on resize


In my React JS website I have a div which acts as an underline in the navbar showing what ever route the user is at.

Here it is here

The div underline element moves over to a new route whenever it is clicked on by the user using css transitions.

The problem is whenever I resize the window the navbar resizes moving the clickable routes as well (which I want it to do). However the div underline element stays in the same spot so is now underlining a random part of the navbar.

Like this

My question is how do I constantly keep the div under the selected route even on resize. Here is my code below:

React:

import { useEffect, useState, useRef } from "react"
import { useLocation } from "react-router-dom"

export default function Underline() {

  let pathname = useLocation().pathname
  
  const [underlineStyle, setUnderlineStyle] = useState()
  const width = useRef(window.innerWidth)

  useEffect(() => {

    function handleResize() {
      width.current = window.innerWidth
    }

    window.addEventListener("resize", handleResize())

    let underlinedLink = document.querySelector(`a[href="${pathname}"]`)

    //as there are 2 anchor tags for home in nav
    if (underlinedLink.getAttribute("id") !== null) { //both home anchor tags are only ones with id's in nav
      underlinedLink = document.querySelector("#home")
    }

    setUnderlineStyle({
      width: `${underlinedLink.offsetWidth}px`,
      left: `${underlinedLink.getBoundingClientRect().left}px`,
    })
  
    return () => {
      window.removeEventListener("resize", handleResize())
    }

  }, [pathname, width.current])

  return <div id="underline" style={underlineStyle}></div>
}

CSS:

#underline {
  margin: 0 0 5px 0;
  height: 4px;
  bottom: 0;
  border: 0;
  position: absolute;
  transition: left 0.3s ease-in-out, width 0.3s ease-in-out;
  background: linear-gradient(to bottom, #48a0e0 20%, #2b58a5);
  box-shadow: 0 -14px 14px #48a0e0;
}

I have tried adding resize event listeners that call a function to move the underline which technically worked but it doesn't constantly stay with the route and only moves to the route when I am done resizing which would be a very strange user experience.

I am also aware I could put the underline div into an ancestor element with both the underline div and the route but that seems like an overly complicated way to do it and am curious if anyone knows any simpler ways to do it. Any suggestions or solutions would be much appreciated.


Solution

  • Create an updateUnderline function inside the useEffect. Call the function immediately when pathname changes, and use it as the resize handler as well.

    Note: if you're using a transition on the underline, remove it while resizing.

    Working Example:

    const { useState, useEffect } = React
    
    function Underline({ pathname }) {
      const [underlineStyle, setUnderlineStyle] = useState()
    
      useEffect(() => {
        function updateUnderline(evt) {
          const underlinedLink = document.querySelector(`a[href="${pathname}"]`)
    
          setUnderlineStyle({
            width: `${underlinedLink.offsetWidth}px`,
            left: `${underlinedLink.getBoundingClientRect().left}px`,
            ...!!evt && { transition: 'none' }
          })
        }
    
        updateUnderline() // call whenever the useEffect is triggered
    
        window.addEventListener("resize", updateUnderline) // use as event handler
    
        return () => {
          window.removeEventListener("resize", updateUnderline)
        }
    
      }, [pathname])
      
    
      return (
        <div id="underline" style={underlineStyle} />
      )
    }
    
    function Links({ links }) {
      const [pathname, setPathname] = useState(links[0])
      
      return (
        <div className="container">
          <div>{
            links.map(href => (
            <a 
              key={href} 
              href={href} 
              onClick={() => setPathname(href)}>{href}
            </a>
          ))}
          </div>
          <Underline pathname={pathname} />
        </div>
      )
    }
    
    ReactDOM
      .createRoot(root)
      .render(<Links links={['#aaaa', '#bbbb', '#cccc', '#dddd']} />)
    .container {
      text-align: center;
    }
    
    a {
      display: inline-block;
      margin: 0 1em;
      text-decoration: none;
    }
    
    #underline { 
      position: absolute;
      border-bottom: 2px solid red; 
      transition: left 0.3s;
    }
    <script crossorigin src="https://unpkg.com/react@18/umd/react.development.js"></script>
    <script crossorigin src="https://unpkg.com/react-dom@18/umd/react-dom.development.js"></script>
    
    <div id="root"></div>

    Code with comments:

    export default function Underline() {
      const pathname = useLocation().pathname
      const [underlineStyle, setUnderlineStyle] = useState()
      
      useEffect(() => {
        function updateUnderline(evt) {
          let underlinedLink = document.querySelector(`a[href="${pathname}"]`)
    
          //as there are 2 anchor tags for home in nav
          if (underlinedLink.hasAttribute("id")) { //both home anchor tags are only ones with id's in nav
            underlinedLink = document.querySelector("#home")
          }
    
          setUnderlineStyle({
            width: `${underlinedLink.offsetWidth}px`,
            left: `${underlinedLink.getBoundingClientRect().left}px`,
            ...!!evt && { transition: 'none' } // remove transition when resizing
          })
        }
        
        updateUnderline() // call whenever the useEffect is triggered
        
        window.addEventListener("resize", updateUnderline) // use as event handler
      
        return () => {
          window.removeEventListener("resize", updateUnderline)
        }
    
      }, [pathname])
    
      return <div id="underline" style={underlineStyle}></div>
    }