Search code examples
javascriptreact-routerreact-router-domchakra-ui

How to style active link in react-router-dom v6


I want the active link to remain as the hovered style. This current styling was done using Chakra UI.

enter image description here

SideNave.jsx

import React, { useState, useEffect } from "react";
import axios from "axios";
import {
  Box,
  CloseButton,
  Flex,
  Hide,
  Image,
  Icon,
  useColorModeValue,
  Drawer,
  DrawerContent,
  useDisclosure
} from "@chakra-ui/react";
import { FiCompass, FiSettings } from "react-icons/fi";
import { NavLink, useLocation } from "react-router-dom";
import {
  AiFillClockCircle,
  AiOutlineTransaction,
  AiOutlineDashboard
} from "react-icons/ai";
import { IoApps, IoLockOpen, IoAnalytics } from "react-icons/io5";
import {
  BsFillPeopleFill,
  BsFillPersonFill,
  BsBookHalf,
  BsCurrencyBitcoin
} from "react-icons/bs";

const LinkItems = [
  {
    name: "Overview",
    icon: AiOutlineDashboard
  },
  { name: "Apps", icon: IoApps },
  { name: "Customers", icon: BsFillPeopleFill },
  { name: "Financial data", icon: IoAnalytics, hasDropdown: true },
  { name: "More", icon: FiSettings, hasDropdown: true }
];

function SideNav() {
  const token = sessionStorage.getItem("token");

  const { isOpen, onOpen, onClose } = useDisclosure();

  return (
    <Box bg={useColorModeValue("gray.100", "gray.900")}>
      <SidebarContent
        onClose={() => onClose}
        display={{ base: "none", md: "block" }}
      />
      <Drawer
        isOpen={isOpen}
        placement="left"
        onClose={onClose}
        returnFocusOnClose={false}
        onOverlayClick={onClose}
        size="full"
      >
        <DrawerContent>
          <SidebarContent onClose={onClose} />
        </DrawerContent>
      </Drawer>
      {/* mobilenav <MobileNav display={{ base: "flex", md: "none" }} onOpen={onOpen} /> */}

      <Box ml={{ base: 0, md: 60 }} p={{ base: 0, md: "4" }}>
        {/* Content */}
      </Box>
    </Box>
  );
}

function SidebarContent({ onClose, post, ...rest }) {
  return (
    <Box
      bg="whiteAlpha.800"
      borderRight="1px"
      borderRightColor={useColorModeValue("gray.200", "gray.700")}
      w={{ base: "full", md: 48, lg: 44, xl: 48, "2xl": 60 }}
      pos="fixed"
      overflow="auto"
      sx={{
        "&::-webkit-scrollbar": {
          width: "6px",
          backgroundColor: "#f1f1f1",
          borderRadius: "68px"
        },
        "&::-webkit-scrollbar-thumb": {
          backgroundColor: "#c1c1c1",
          borderRadius: "68px"
        }
      }}
      h="full"
      {...rest}
    >
      <Flex
        h="20"
        mb={12}
        alignItems="center"
        mx="8"
        justifyContent="space-between"
      >
        <CloseButton display={{ base: "flex", md: "none" }} onClick={onClose} />
      </Flex>
      {LinkItems.map((link) => (
        <React.Fragment key={link.name}>
          {link.name === "Financial data" || link.name === "More" ? (
            <NavItem icon={link.icon} hasDropdown={link.hasDropdown}>
              {link.name}
            </NavItem>
          ) : (
            <div className="nav">
              <NavLink
                to={`/dashboard/${link.name.toLowerCase().replace(" ", "-")}`}
              >
                <NavItem icon={link.icon} hasDropdown={link.hasDropdown}>
                  {link.name}
                </NavItem>
              </NavLink>
            </div>
          )}
        </React.Fragment>
      ))}
    </Box>
  );
}

function NavItem({ icon, children, hasDropdown, ...rest }) {
  const [isDropdownOpen, setIsDropdownOpen] = useState(false);

  const handleDropdownToggle = () => {
    setIsDropdownOpen(!isDropdownOpen);
  };

  return (
    <Box
      as="div"
      style={{ textDecoration: "none" }}
      _focus={{ boxShadow: "none" }}
      mt={4}
    >
      <Flex
        align="center"
        p="2"
        mx="2"
        borderRadius="lg"
        role="group"
        fontSize="15px"
        cursor="pointer"
        _hover={{
          bg: "#002c8a",
          color: "white",
          borderColor: "white"
        }}
        onClick={hasDropdown ? handleDropdownToggle : undefined}
        {...rest}
      >
        {icon && (
          <Icon
            mr="4"
            p={1}
            border="1px"
            borderRadius="full"
            borderColor="gray.400"
            fontSize="24"
            as={icon}
          />
        )}
        {children}
      </Flex>
      {hasDropdown && isDropdownOpen && <DropdownOptions name={children} />}
    </Box>
  );
}

function DropdownOptions({ name }) {
  const options = getDropdownOptions(name);

  const location = useLocation();

  return (
    <Flex
      flexDirection="column"
      ml="4"
      boxShadow="lg"
      w="150px"
      p={2}
      borderRadius="lg"
    >
      {options.map((option) => (
        <NavLink
          key={option.text}
          to={`/dashboard/${option.text.toLowerCase()}`}
        >
          <Box
            key={option}
            className={`dropdown-option ${
              location.pathname.includes(option.text.toLowerCase())
                ? "active-dot"
                : ""
            }`}
            as="div"
            py="2"
            px="1"
            borderRadius="lg"
            w="130px"
            fontSize="14px"
            _hover={{
              bg: "#002c8a",
              color: "white"
            }}
          >
            {option.icon && (
              <Icon
                mr="2"
                fontSize="14"
                verticalAlign="middle"
                as={option.icon}
              />
            )}
            {option.text}
          </Box>
        </NavLink>
      ))}
    </Flex>
  );
}

function getDropdownOptions(name) {
  if (name === "Financial data") {
    return [
      {
        text: "Records",
        icon: AiFillClockCircle
      },
      {
        text: "Transactions",
        icon: AiOutlineTransaction
      },
      { text: "Balance", icon: BsBookHalf },
      {
        text: "Auth",
        icon: IoLockOpen
      }
    ];
  } else if (name === "More") {
    return [
      { text: "Logs", icon: IoAnalytics },
      { text: "Billings", icon: BsCurrencyBitcoin },
      { text: "Teams", icon: FiCompass },
      { text: "Profile", icon: BsFillPersonFill }
    ];
  }

  return [];
}

export default SideNav;

I created a minimal recreation here: https://codesandbox.io/s/navlink-project-n4d6cs?file=/SideNav.jsx


Solution

  • Use the NavLink component's children function to pass through the link's isActive state to the NavItem component, then conditionally apply the "hover" style.

    <NavLink
      to={`/dashboard/${link.name.toLowerCase().replace(" ", "-")}`}
    >
      {({ isActive }) => ( // <-- isActive prop
        <NavItem
          icon={link.icon}
          hasDropdown={link.hasDropdown}
          isActive={isActive} // <-- pass through
        >
          {link.name}
        </NavItem>
      )}
    </NavLink>
    

    NavItem

    function NavItem({ icon, children, hasDropdown, isActive, ...rest }) { // <-- consume
      const [isDropdownOpen, setIsDropdownOpen] = useState(false);
    
      const handleDropdownToggle = () => {
        setIsDropdownOpen(!isDropdownOpen);
      };
    
      return (
        <Box
          as="div"
          style={{ textDecoration: "none" }}
          _focus={{ boxShadow: "none" }}
          mt={4}
        >
          <Flex
            align="center"
            p="2"
            mx="2"
            borderRadius="lg"
            role="group"
            fontSize="15px"
            cursor="pointer"
            _hover={{
              bg: "#002c8a",
              color: "white",
              borderColor: "white"
            }}
            onClick={hasDropdown ? handleDropdownToggle : undefined}
            {...(isActive // <-- conditional application
              ? {
                  bg: "#002c8a",
                  color: "white",
                  borderColor: "white"
                }
              : {})}
            {...rest}
          >
            {icon && (
              <Icon
                mr="4"
                p={1}
                border="1px"
                borderRadius="full"
                borderColor="gray.400"
                fontSize="24"
                as={icon}
              />
            )}
            {children}
          </Flex>
          {hasDropdown && isDropdownOpen && <DropdownOptions name={children} />}
        </Box>
      );
    }
    

    Edit how-to-style-active-link-in-react-router-dom-v6