DEV Community

Ilia Mikhailov
Ilia Mikhailov

Posted on • Originally published at codechips.me on

Tailwind UI dropdown menu - React vs Svelte

The other day I was prototyping a new internal app at work in React using Next.js. To get it off the ground quickly I used Tailwind CSS. In my app I needed to create a simple dropdown menu and I looked at the Tailwind UI example on how they did it.

Actually, creating a dropdown menu is not as simple as it sounds. First, you have to handle mouse clicks outside of it and close the menu if it's currently open. Second, you should support pressing Escape key and close the menu if it's currently open. Third, you should add nice animation to the menu so it feels more alive.

Tailwind UI dropdown meny example

Implementing the menu in React wasn't quite as straight forward as I hoped for. The Tailwind styling itself is not a problem, but it took me some time to figure out how to handle "click away" or "click outside" functionality and handling the escape key. On top of that I had to research how to do CSS transitions in React. Turns out that creators of Tailwind created a helpful transition library as React does not have the functionality built-in.

Doing a Google search for "react click away listener" didn't really help. A search on NPM for "react click outside" and "react click away" returned way too many more results than I needed. Sure, there are plenty of React libraries, but I felt that there should be a much simpler way of handling that.

Here is the Next.js (React + TypeScript) code I ended up with.

import Link from 'next/link';
import React, { useState, useRef, useEffect } from 'react';
import { Transition } from '@tailwindui/react';

const Menu = ({ user }) => {
  const [show, setShow] = useState(false);
  const container = useRef(null);

  useEffect(() => {
    const handleOutsideClick = (event: MouseEvent) => {
      if (!container.current.contains(event.target)) {
        if (!show) return;
        setShow(false);
      }
    };

    window.addEventListener('click', handleOutsideClick);
    return () => window.removeEventListener('click', handleOutsideClick);
  }, [show, container]);

  useEffect(() => {
    const handleEscape = (event: KeyboardEvent) => {
      if (!show) return;

      if (event.key === 'Escape') {
        setShow(false);
      }
    };

    document.addEventListener('keyup', handleEscape);
    return () => document.removeEventListener('keyup', handleEscape);
  }, [show]);

  return (
    <div ref={container} className="relative">
      <button
        className="menu focus:outline-none focus:shadow-solid "
        onClick={() => setShow(!show)}
      >
        <img
          className="w-10 h-10 rounded-full"
          src={user.picture}
          alt={user.name}
        />
      </button>

      <Transition
        show={show}
        enter="transition ease-out duration-100 transform"
        enterFrom="opacity-0 scale-95"
        enterTo="opacity-100 scale-100"
        leave="transition ease-in duration-75 transform"
        leaveFrom="opacity-100 scale-100"
        leaveTo="opacity-0 scale-95"
      >
        <div className="origin-top-right absolute right-0 w-48 py-2 mt-1 bg-gray-800 rounded shadow-md">
          <Link href="/profile">
            <a className="block px-4 py-2 hover:bg-green-500 hover:text-green-100">
              Profile
            </a>
          </Link>
          <Link href="/api/logout">
            <a className="block px-4 py-2 hover:bg-green-500 hover:text-green-100">
              Logout
            </a>
          </Link>
        </div>
      </Transition>
    </div>
  );
};

export default Menu;
Enter fullscreen mode Exit fullscreen mode

When I was done with React implementation, I thought to myself of how I would implement the same menu in Svelte. So I took some time to port it to Svelte.

One of the many niceties about Svelte is that it has CSS transitions and animations built in. Here is my take on it.

<script>
  import { onMount } from 'svelte';
  import { scale } from 'svelte/transition';

  export let user;

  let show = false; // menu state
  let menu = null; // menu wrapper DOM reference

  onMount(() => {
    const handleOutsideClick = (event) => {
      if (show && !menu.contains(event.target)) {
        show = false;
      }
    };

    const handleEscape = (event) => {
      if (show && event.key === 'Escape') {
        show = false;
      }
    };

    // add events when element is added to the DOM
    document.addEventListener('click', handleOutsideClick, false);
    document.addEventListener('keyup', handleEscape, false);

    // remove events when element is removed from the DOM
    return () => {
      document.removeEventListener('click', handleOutsideClick, false);
      document.removeEventListener('keyup', handleEscape, false);
    };
  });
</script>

<div class="relative" bind:this={menu}>
  <div>
    <button
      on:click={() => (show = !show)}
      class="menu focus:outline-none focus:shadow-solid"
    >
      <img class="w-10 h-10 rounded-full" src={user.picture} alt={user.name} />
    </button>

    {#if show}
      <div
        in:scale={{ duration: 100, start: 0.95 }}
        out:scale={{ duration: 75, start: 0.95 }}
        class="origin-top-right absolute right-0 w-48 py-2 mt-1 bg-gray-800
          rounded shadow-md"
      >
        <a
          href="/profile"
          class="block px-4 py-2 hover:bg-green-500 hover:text-green-100"
        >Profile</a>
        <a
          href="/api/logout"
          class="block px-4 py-2 hover:bg-green-500 hover:text-green-100"
        >Logout</a>
      </div>
    {/if}
  </div>
</div>
Enter fullscreen mode Exit fullscreen mode

Sure, the amount of code is little less in Svelte than in React, but what about the cognitive load? Which one is easier to read and understand? You be the judge.

Top comments (0)