Let’s start with a fresh Next.js project. We are going to use bun as our package manager (because it’s super fast, even faster pnpm). In your vs code terminal, navigate to your desired folder and run:
bun create next-app my-dropdown
Now navigate to the folder my-dropdown. Inside the src folder we will create a new components folder, where we will create our first dropdown component.
So create a file named dropdown.tsx and start putting in some stuffs. Let’s create the basic structure of the dropdown component:
"use client";
interface DropdownProps {}
export function Dropdown({}: DropdownProps) {
return <div>Dropdown</div>;
}
We want to make sure that the component is type-safe. So let’s create an interface and a DropdownItem type. Create a new folder called types inside src, then create a new index.ts file.
export type DropdownItem = {
label: string;
value: string;
};
Now in our dropdown component again -
"use client";
import { DropdownItem } from "@/types";
interface DropdownProps {
options: DropdownItem[];
}
export function Dropdown({ options }: DropdownProps) {
return <div>Dropdown</div>;
}
Now that we have the basic structure, let’s put in some design -
"use client";
import { DropdownItem } from "@/types";
interface DropdownProps {
options: DropdownItem[];
name: string;
}
export function Dropdown({ options }: DropdownProps) {
return (
<div>
<div className="relative inline-block text-left">
<div>
<button
type="button"
className="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
id="menu-button"
aria-expanded="true"
aria-haspopup="true"
>
Options
<svg
className="-mr-1 h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
<div
className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabIndex={-1}
>
<div className="py-1" role="none">
<a
href="#"
className="text-gray-700 block px-4 py-2 text-sm"
role="menuitem"
tabIndex={-1}
id="menu-item-0"
>
Account settings
</a>
<a
href="#"
className="text-gray-700 block px-4 py-2 text-sm"
role="menuitem"
tabIndex={-1}
id="menu-item-1"
>
Support
</a>
<a
href="#"
className="text-gray-700 block px-4 py-2 text-sm"
role="menuitem"
tabIndex={-1}
id="menu-item-2"
>
License
</a>
<form method="POST" action="#" role="none">
<button
type="submit"
className="text-gray-700 block w-full px-4 py-2 text-left text-sm"
role="menuitem"
tabIndex={-1}
id="menu-item-3"
>
Sign out
</button>
</form>
</div>
</div>
</div>
</div>
);
}
Now go to page.tsx and render your component and test the work so far:
import { Dropdown } from "@/components/dropdown";
export default function Home() {
const options = [
{ label: "Account settings", value: "account" },
{ label: "Support", value: "support" },
{ label: "License", value: "license" },
];
return (
<div className="flex justify-center items-center h-screen">
<Dropdown name="Options" options={options} />
</div>
);
}
Run the application with -
bun dev
You should now be able to see the current state:
Great work so far! Now let’s my our dropdown dynamic. Navigate to the dropdown component and do the following:
"use client";
import { DropdownItem } from "@/types";
import Link from "next/link";
import { useState } from "react";
interface DropdownProps {
options: DropdownItem[];
name: string;
}
export function Dropdown({ options, name }: DropdownProps) {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<div className="relative inline-block text-left">
<div>
<button
onClick={() => setIsOpen(!isOpen)}
type="button"
className="inline-flex w-full justify-center gap-x-1.5 rounded-md bg-white px-3 py-2 text-sm font-semibold text-gray-900 shadow-sm ring-1 ring-inset ring-gray-300 hover:bg-gray-50"
id="menu-button"
aria-expanded="true"
aria-haspopup="true"
>
{name}
<svg
className="-mr-1 h-5 w-5 text-gray-400"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fill-rule="evenodd"
d="M5.23 7.21a.75.75 0 011.06.02L10 11.168l3.71-3.938a.75.75 0 111.08 1.04l-4.25 4.5a.75.75 0 01-1.08 0l-4.25-4.5a.75.75 0 01.02-1.06z"
clip-rule="evenodd"
/>
</svg>
</button>
</div>
{isOpen && (
<div
className="absolute right-0 z-10 mt-2 w-56 origin-top-right rounded-md bg-white shadow-lg ring-1 ring-black ring-opacity-5 focus:outline-none"
role="menu"
aria-orientation="vertical"
aria-labelledby="menu-button"
tabIndex={-1}
>
<div className="py-1" role="none">
{options.map((option) => (
<Link
key={option.value}
href={`/${option.value}`}
className="text-gray-700 block px-4 py-2 text-sm"
role="menuitem"
tabIndex={-1}
id="menu-item-0"
>
{option.label}
</Link>
))}
</div>
</div>
)}
</div>
</div>
);
}
And that’s it. You can now define your own options and dropdown name and test the component out!
Here’s the final result:
Thank you for reading this far! Good luck!