Easy Dropdown Menus With Next.js and Tailwind CSS
Tags: webdev, react, nextjs, frontend
Published: 2023-05-03 14:34:44

Tailwind CSS is a utility-first CSS framework that has gained immense popularity among developers in recent years. One of the reasons why Tailwind is cool is its focus on providing a simple and intuitive way to style web applications. With Tailwind, developers can rapidly prototype and build custom user interfaces without having to write custom CSS. Tailwind offers a wide range of pre-built classes that can be used to style various UI components, such as buttons, forms, and grids, with ease.

It's a no-brainer for component-oriented design because it encourages the separation of concerns between structure and style. With Tailwind, components can be defined using simple HTML markup, and styles can be applied using utility classes. This approach allows developers to focus on the functional aspects of their components, without worrying about the visual styling. By decoupling the structure and style, Tailwind makes it easier to reuse components across an application, improving the overall maintainability of the codebase.

In this article, I'm going to show you how to create a very simple, yet elegant, dropdown menu using nothing but Next.js and Tailwind. For a working demo, please check out the link at the bottom of the page.

---

## The Dropdown Component

We'll begin by creating our reusable component:

```tsx
import React, { useState } from 'react'
import Link from 'next/link';

import { MenuItem } from './Header';

interface Props {
    item: MenuItem;
}

export default function Dropdown(props: Props) {
    const { item } = props;
    const [isOpen, setIsOpen] = useState<boolean>(false);
    const menuItems = item?.children ? item.children : [];

    const toggle = () => {
        setIsOpen(old => !old);
    }

    const transClass = isOpen
        ?
        "flex"
        :
        "hidden";

    return (
        <>
            <div className="relative">
                <button
                    className="hover:text-blue-400"
                    onClick={toggle}
                >{item.title}</button>
                <div className={`absolute top-8 z-30 w-[250px] min-h-[300px] flex flex-col py-4 bg-zinc-400 rounded-md ${transClass}`}>
                    {
                        menuItems.map(item =>
                            <Link
                                key={item.route}
                                className="hover:bg-zinc-300 hover:text-zinc-500 px-4 py-1"
                                href={item?.route || ''}
                                onClick={toggle}
                            >{item.title}</Link>
                        )
                    }
                </div>
            </div>
            {
                isOpen
                    ?
                    <div
                        className="fixed top-0 right-0 bottom-0 left-0 z-20 bg-black/40"
                        onClick={toggle}
                    ></div>
                    :
                    <></>
            }
        </>
    )
}
```

As you can see, this component maintains its own `isOpen` state. We render a modal overlay with a slight tint to 1. focus the user's attention on the open menu, and 2. serve as a means to close the menu when the user clicks off of it. (You know how annoying it is when menus don't close when you click off of them! 😡)

We set the parent div to `relative` and the dropdown as `absolute`. Lastly, we conditionally set the class to either `flex` or `hidden` depending on our state.

---

## The Header Component

Now we'll create a header component to contain our menu bar with regular links and dropdown menus.

```tsx
import React from "react";
import Image from "next/image";
import Link from "next/link";
import Dropdown from "./Dropdown";

export interface MenuItem {
  title: string;
  route?: string;
  children?: MenuItem[];
}

const menuItems: MenuItem[] = [
  {
    title: "Home",
    route: "/",
  },
  {
    title: "Products",
    children: [
      {
        title: "Hinkle Horns",
        route: "/products/hinkle-horns",
      },
      {
        title: "Doozers",
        route: "/products/doozers",
      },
      {
        title: "Zizzer-zazzers",
        route: "/products/zizzer-zazzers",
      },
    ],
  },
];

export default function Header() {
  return (
    <header className="flex gap-10 items-center bg-zinc-800 py-4 px-2">
      <Link href="https://designly.biz" target="_blank">
        <Image src="/logo.svg" width={200} height={20} alt="logo" />
      </Link>
      <div className="flex gap-8 items-center text-white">
        {menuItems.map((item) => {
          return item.hasOwnProperty("children") ? (
            <Dropdown item={item} />
          ) : (
            <Link className="hover:text-blue-500" href={item?.route || ""}>
              {item.title}
            </Link>
          );
        })}
      </div>
    </header>
  );
}
```

As you can see, we conditionally render either a `<Link>` component or a `<Dropdown>` component depending on the existence of the `children` prop. Also, note that our `MenuItem` type is a recursive object.

---

That's about it. Check out the demo for a full implementation of this code. You should end up with something like this:

![End Result](https://cdn.designly.biz/blog_files/easy-dropdown-menus-with-next-js-and-tailwind-css/image1.jpg)

[Code Sandbox Demo](https://codesandbox.io/p/sandbox/withered-hooks-6oikyz?file=%2Fcomponents%2FDropdown.tsx&selection=%5B%7B%22endColumn%22%3A37%2C%22endLineNumber%22%3A45%2C%22startColumn%22%3A37%2C%22startLineNumber%22%3A45%7D%5D)

---

Thank you for taking the time to read my article and I hope you found it useful (or at the very least, mildly entertaining). For more great information about web dev, systems administration and cloud computing, please read the [Designly Blog](https://designly.biz/blog). Also, please leave your comments! I love to hear thoughts from my readers.

I use [Hostinger](https://hostinger.com?REFERRALCODE=1J11864) to host my clients' websites. You can get a business account that can host 100 websites at a price of $3.99/mo, which you can lock in for up to 48 months! It's the best deal in town. Services include PHP hosting (with extensions), MySQL, Wordpress and Email services.

Looking for a web developer? I'm available for hire! To inquire, please fill out a [contact form](https://designly.biz/contact).