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.
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;
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>
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)