Skip to content
Drivn
6 min read

How to Add a Dropdown to a React App

Step-by-step guide to adding a copy-and-own dropdown menu to any React project with the Drivn CLI — dot-notation API, outside-click dismiss, no portal.

A dropdown menu is a small panel that opens under a trigger button — an account menu in a header, a row of actions on a table, a kebab on a card. Building one well means tracking open state, positioning the panel under the button, closing it when you click anywhere else, and animating the open and close. Most teams reach for a floating-position library and a portal on top of a menu primitive to get all of that, and every dependency adds runtime JavaScript.

Drivn writes the component into your repository instead. The CLI copies a dropdown.tsx file built on plain React — useState tracks open and closed, a single mousedown listener on the document closes the menu on outside-click, and a CSS transition on opacity and scale handles the animation. There is no floating-ui, no portal, and no menu library in your package.json. The API is dot notation: Dropdown.Trigger opens, Dropdown.Content holds the panel, and Dropdown.Item is a single action with an icon prop and a destructive flag. After install the file is yours to read and restyle. Because it holds state with hooks and listens for outside clicks in an effect, it is a client component marked 'use client', while the page around it can still render on the server.

This guide adds the Dropdown to an existing React app in about ten minutes — install the CLI, render a basic actions menu, group items with labels and separators, and align the panel to either edge. It works the same in Vite + React or a Next.js App Router project. For the Next.js-specific walkthrough see the Next.js Dropdown guide; for live patterns see the Dropdown examples.

Prerequisites

Before installing the Dropdown, confirm your React project has the three things Drivn assumes: Tailwind CSS v4 installed and processing your CSS, TypeScript configured (the component ships as a .tsx file), and a @/ path alias pointing at your source directory. If you scaffolded with create-next-app, npm create vite, or npx drivn@latest create, all three are already wired. For a custom setup, check the compilerOptions.paths entry in tsconfig.json; the installation page lists the minimal config. The Dropdown composes Drivn's own Button for its trigger, so the CLI writes that file too, and it adds lucide-react to your package.json if it is missing — you use the icons on menu items via the icon prop.

Step 1 — Install Drivn via the CLI

Run the CLI from your project root to add the Dropdown source file. The command prompts once for your install directory (defaulting to src/components/ui/), writes dropdown.tsx, and resolves its component dependency by also writing button.tsx, since the Dropdown imports Button directly for its trigger. It adds lucide-react to your package.json if it is not already present. No global config file is created — both files are TypeScript you edit like any other component. Confirm they landed in your editor, then commit them. If your project uses a monorepo layout or a non-standard path, the CLI docs cover the flags for targeting a custom location during install.

1# add the Dropdown (the CLI also writes its Button dependency)
2npx drivn add dropdown
3
4# verify both files were written
5ls src/components/ui/dropdown.tsx src/components/ui/button.tsx

Step 2 — Render a basic actions menu

Open the page where the Dropdown should live and import it from your UI directory. The compound API is Dropdown, Dropdown.Trigger, Dropdown.Content, Dropdown.Item, Dropdown.Group, Dropdown.Label, and Dropdown.Separator. In the simplest form the trigger opens the panel, each Dropdown.Item takes a Lucide component reference on its icon prop and renders it at w-4 h-4, and the menu closes on selection — the Item source calls setOpen(false) right after your onClick runs, so you write no close logic. Set destructive on the last item to switch it to the destructive text color and a red-tinted hover. Because the Dropdown holds open state with useState, the source begins with 'use client' — in a Next.js App Router project keep this in a small client component and render it as an island. See the Dropdown docs for the full prop table.

1'use client'
2import { Dropdown } from '@/components/ui/dropdown'
3import { Edit, Copy, Trash2 } from 'lucide-react'
4
5export function RowActions() {
6 return (
7 <Dropdown>
8 <Dropdown.Trigger>Actions</Dropdown.Trigger>
9 <Dropdown.Content>
10 <Dropdown.Item icon={Edit} onClick={() => console.log('edit')}>
11 Edit
12 </Dropdown.Item>
13 <Dropdown.Item icon={Copy} onClick={() => console.log('copy')}>
14 Duplicate
15 </Dropdown.Item>
16 <Dropdown.Separator />
17 <Dropdown.Item icon={Trash2} destructive onClick={() => console.log('delete')}>
18 Delete
19 </Dropdown.Item>
20 </Dropdown.Content>
21 </Dropdown>
22 )
23}

Step 3 — Group items with labels and separators

A real menu is more than a flat list. Dropdown.Group wraps a related set with a py-1 rhythm, Dropdown.Label renders a small muted heading above a group at px-3 py-1.5 text-xs font-medium text-muted-foreground, and Dropdown.Separator draws a one-pixel divider (my-1 h-px bg-border) between sections. The pattern matches how Linear and Notion structure their context menus — a label sets context, the items act on it, a separator breaks to the next group. Keep each group to three to five items; if a group grows past five the menu is doing too much and the actions belong in Tabs or a Sidebar instead. The structure below composes an account menu with a labeled group, a separator, and a destructive sign-out. For live variations see the Dropdown examples.

1<Dropdown.Content>
2 <Dropdown.Group>
3 <Dropdown.Label>Account</Dropdown.Label>
4 <Dropdown.Item icon={User}>Profile</Dropdown.Item>
5 <Dropdown.Item icon={Settings}>Settings</Dropdown.Item>
6 </Dropdown.Group>
7 <Dropdown.Separator />
8 <Dropdown.Item icon={LogOut} destructive>
9 Sign out
10 </Dropdown.Item>
11</Dropdown.Content>

Step 4 — Align the menu and customize styles

The root takes an align prop — 'left' (default) or 'right' — that indexes a tiny styles.align map setting left-0 or right-0 on the panel. Use align="right" when the trigger sits near the right edge of the viewport so the menu does not overflow, such as a table's trailing actions column or a header account menu. Everything else is a class string in the styles object at the top of the file you own: content sets the panel surface (bg-card border border-border rounded-[10px] p-1 with min-w-[180px] and shadow-lg), item sets the row, and destructive sets the red variant. Edit any of them once and every dropdown in your app updates, and because the colors read from your Tailwind tokens, changing them in theming re-themes the menu for dark and light mode. The content and align slices below are verbatim from dropdown.ts.

1content: cn(
2 'absolute top-full mt-1 min-w-[180px] z-50',
3 'bg-card border border-border rounded-[10px] p-1',
4 'shadow-lg',
5 'transition-[opacity,scale] duration-150 ease-out'
6),
7align: {
8 left: 'left-0',
9 right: 'right-0',
10},
Get started

Install Drivn in one command

Copy the source into your project and own every line. Zero runtime dependencies, pure React + Tailwind.

Follow Drivn updates
New components, improvements, and guides every release.
Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

Yes. Set the project up with TypeScript and Tailwind v4 first, then run npx drivn add dropdown. The Dropdown has no dependency on Next.js or any router — it renders anywhere React and Tailwind reach the DOM. Vite + React is the most common non-Next setup and works without extra configuration; the CLI also writes the Button dependency the Dropdown uses for its trigger and adds lucide-react for the menu-item icons.

The Dropdown is a client component — it is marked 'use client' because it tracks open state with useState and listens for outside clicks in an effect. Your surrounding page and layout still render on the server. The usual pattern is a small client component holding the Dropdown and its trigger, rendered as an island inside an otherwise server-rendered route. Next.js inserts the client boundary at the import automatically, with no dynamic() import required.

No. The panel is a plain absolute top-full mt-1 element positioned directly under the trigger inside the same wrapper <div>, so there is no portal and no floating-ui or popper dependency. Dismissal is a single mousedown listener on the document that closes the menu when the click lands outside the wrapper ref. The whole behavior lives in the one file the CLI writes, plus Drivn's own Button for the trigger.

You do not have to wire anything — the Item source calls setOpen(false) immediately after running your onClick handler, so selecting any item closes the menu automatically. Clicking the trigger again or clicking anywhere outside the wrapper also closes it. If you need an item that keeps the menu open, that is a small edit to the Item function in the source file you own after install.

No — the Drivn Dropdown is a click-driven menu. It opens on trigger click, closes on outside-click or item-click, and does not handle arrow keys, typeahead, Escape, submenus, or checkbox and radio items. If your menu needs the full ARIA keyboard pattern, pick shadcn/ui's Radix-backed DropdownMenu for that surface — see Drivn vs shadcn Dropdown. The two libraries can coexist in the same project.