Skip to content

Instantly share code, notes, and snippets.

@belachkar
Created February 1, 2021 00:05
Show Gist options
  • Save belachkar/93fb8a1195c5ad003689760f405eeda3 to your computer and use it in GitHub Desktop.
Save belachkar/93fb8a1195c5ad003689760f405eeda3 to your computer and use it in GitHub Desktop.

Advanced dropdown facebook animation

Created using react-transition-group package.

App.js:

import { useEffect, useRef, useState } from 'react';
import { CSSTransition } from 'react-transition-group';
import { ReactComponent as ArrowIcon } from './icons/arrow.svg';
import { ReactComponent as BellIcon } from './icons/bell.svg';
import { ReactComponent as BoltIcon } from './icons/bolt.svg';
import { ReactComponent as CaretIcon } from './icons/caret.svg';
import { ReactComponent as ChevronIcon } from './icons/chevron.svg';
import { ReactComponent as CogIcon } from './icons/cog.svg';
import { ReactComponent as MessengerIcon } from './icons/messenger.svg';
import { ReactComponent as PlusIcon } from './icons/plus.svg';

function App() {
  return (
    <Navbar>
      <NavItem icon={<PlusIcon />} />
      <NavItem icon={<BellIcon />} />
      <NavItem icon={<MessengerIcon />} />

      <NavItem icon={<CaretIcon />}>
        <DropdownMenu></DropdownMenu>
      </NavItem>
    </Navbar>
  );
}

function Navbar({ children }) {
  return (
    <nav className="navbar">
      <ul className="navbar-nav">{children}</ul>
    </nav>
  );
}

function NavItem({ icon, children }) {
  const [open, setOpen] = useState(false);

  return (
    <li className="nav-item">
      <a href="!#" className="icon-button" onClick={() => setOpen(!open)}>
        {icon}
      </a>

      {open && children}
    </li>
  );
}

function DropdownMenu() {
  const [activeMenu, setActiveMenu] = useState('main');
  const [menuHeight, setMenuHeight] = useState(null);
  const dropdownRef = useRef(null);

  useEffect(() => {
    setMenuHeight(dropdownRef.current?.firstChild.offsetHeight);
  }, []);

  function calcHeight(el) {
    const height = el.offsetHeight;
    setMenuHeight(height);
  }

  function DropdownItem({ leftIcon, rightIcon, goToMenu, children }) {
    return (
      <a
        href="!#"
        className="menu-item"
        onClick={() => goToMenu && setActiveMenu(goToMenu)}
      >
        {leftIcon && <span className="icon-button">{leftIcon}</span>}
        {children}
        {rightIcon && (
          <span className="icon-button icon-right">{rightIcon}</span>
        )}
      </a>
    );
  }

  return (
    <div className="dropdown" style={{ height: menuHeight }} ref={dropdownRef}>
      <CSSTransition
        in={activeMenu === 'main'}
        timeout={500}
        classNames="menu-primary"
        unmountOnExit
        onEnter={calcHeight}
      >
        <div className="menu">
          <DropdownItem>My Profile</DropdownItem>
          <DropdownItem
            leftIcon={<CogIcon />}
            rightIcon={<ChevronIcon />}
            goToMenu="settings"
          >
            Settings
          </DropdownItem>
          <DropdownItem
            leftIcon="🦧"
            rightIcon={<ChevronIcon />}
            goToMenu="animals"
          >
            Animals
          </DropdownItem>
        </div>
      </CSSTransition>

      <CSSTransition
        in={activeMenu === 'settings'}
        timeout={500}
        classNames="menu-secondary"
        unmountOnExit
        onEnter={calcHeight}
      >
        <div className="menu">
          <DropdownItem goToMenu="main" leftIcon={<ArrowIcon />}>
            <h2>My Tutorial</h2>
          </DropdownItem>
          <DropdownItem leftIcon={<BoltIcon />}>HTML</DropdownItem>
          <DropdownItem leftIcon={<BoltIcon />}>CSS</DropdownItem>
          <DropdownItem leftIcon={<BoltIcon />}>JavaScript</DropdownItem>
          <DropdownItem leftIcon={<BoltIcon />}>Awesome!</DropdownItem>
        </div>
      </CSSTransition>

      <CSSTransition
        in={activeMenu === 'animals'}
        timeout={500}
        classNames="menu-secondary"
        unmountOnExit
        onEnter={calcHeight}
      >
        <div className="menu">
          <DropdownItem goToMenu="main" leftIcon={<ArrowIcon />}>
            <h2>Animals</h2>
          </DropdownItem>
          <DropdownItem leftIcon="🦘">Kangaroo</DropdownItem>
          <DropdownItem leftIcon="🐸">Frog</DropdownItem>
          <DropdownItem leftIcon="🦋">Horse?</DropdownItem>
          <DropdownItem leftIcon="🦔">Hedgehog</DropdownItem>
        </div>
      </CSSTransition>
    </div>
  );
}

export default App;

index.css:

body {
  margin                 : 0;
  background             : #151616;
  font-family            : roboto;
  -webkit-font-smoothing : antialiased;
  -moz-osx-font-smoothing: grayscale;
}

:root {
  --bg           : #242526;
  --bg-accent    : #484a4d;
  --text-color   : #dadce1;
  --nav-size     : 60px;
  --border       : 1px solid #474a4d;
  --border-radius: 8px;
  --speed        : 500ms;
}

ul {
  list-style: none;
  margin    : 0;
  padding   : 0;
}

a {
  color          : var(--text-color);
  text-decoration: none;
  ;
}

/* Top Navigation Bar */

/* <nav> */
.navbar {
  height          : var(--nav-size);
  background-color: var(--bg);
  padding         : 0 1rem;
  border-bottom   : var(--border);
}

/* <ul> */
.navbar-nav {
  max-width      : 100%;
  height         : 100%;
  display        : flex;
  justify-content: flex-end;
}

/* <li> */
.nav-item {
  width          : calc(var(--nav-size) * 0.8);
  display        : flex;
  align-items    : center;
  justify-content: center;
}

/* Icon Button */
.icon-button {
  --button-size   : calc(var(--nav-size) * 0.5);
  width           : var(--button-size);
  height          : var(--button-size);
  background-color: #484a4d;
  border-radius   : 50%;
  padding         : 5px;
  margin          : 2px;
  display         : flex;
  align-items     : center;
  justify-content : center;
  transition      : filter 300ms;
}

.icon-button:hover {
  filter: brightness(1.2);
}

.icon-button svg {
  fill  : var(--text-color);
  width : 20px;
  height: 20px;
}


/* Dropdown Menu */

.dropdown {
  position        : absolute;
  top             : 58px;
  width           : 300px;
  transform       : translateX(-45%);
  background-color: var(--bg);
  border          : var(--border);
  border-radius   : var(--border-radius);
  padding         : 1rem;
  overflow        : hidden;
  transition      : height var(--speed) ease;
}

.menu {
  width: 100%;
}

.menu-item {
  height       : 50px;
  display      : flex;
  align-items  : center;
  border-radius: var(--border-radius);
  transition   : background var(--speed);
  padding      : 0.5rem;
}

.menu-item .icon-button {
  margin-right: 0.5rem;
}


.menu-item .icon-button:hover {
  filter: none;
}

.menu-item:hover {
  background-color: #525357;
}

.icon-right {
  margin-left: auto;
}

/* CSSTransition classes  */
.menu-primary-enter {
  position : absolute;
  transform: translateX(-110%);
}

.menu-primary-enter-active {
  transform : translateX(0%);
  transition: all var(--speed) ease;
}

.menu-primary-exit {
  position: absolute;
}

.menu-primary-exit-active {
  transform : translateX(-110%);
  transition: all var(--speed) ease;
}


.menu-secondary-enter {
  transform: translateX(110%);
}

.menu-secondary-enter-active {
  transform : translateX(0%);
  transition: all var(--speed) ease;
}

.menu-secondary-exit {
  position: absolute;
}

.menu-secondary-exit-active {
  transform : translateX(110%);
  transition: all var(--speed) ease;
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment