Skip to content
Drivn
6 min read

React Dropdown Component Examples

Drop-in React Dropdown examples: actions menu, right-aligned, grouped items, user avatar menu, and table-row kebab. Zero runtime deps, dot notation API.

A dropdown is the right surface for short lists of click-driven actions — Edit, Duplicate, Delete on a Table row, the user-avatar menu in a header, a kebab on a Card. The Dropdown component in Drivn is a plain React file of about 168 lines that ships zero runtime UI dependencies: useState tracks open and closed, a mousedown listener on document closes the menu when you click outside, and a CSS transition on opacity and scale handles the entrance. It uses dot notation — Dropdown.Trigger opens, Dropdown.Content renders the panel, and Dropdown.Item is a single action with an icon prop and a destructive flag.

This page collects the patterns that come up in shipped UIs. Each example is copy-paste ready and uses the same @/components/ui/dropdown import path the Drivn CLI installs under, so the snippets compile the moment you run npx drivn add dropdown. The trigger renders Drivn's Button with variant="outline" and rounded="md" internally, so consumers do not need to compose a styled handle — any children passed to Dropdown.Trigger render as the button label. The align prop on the root accepts left (default) or right and picks the matching styles.align utility on Dropdown.Content.

If your menu needs full keyboard navigation, submenus, or checkbox and radio items, the Drivn Dropdown does not ship those — see Drivn vs shadcn Dropdown for the comparison and pick Radix through shadcn for that surface. The examples below stay focused on the click-driven shape: a basic actions menu with icons, a right-aligned menu, grouped items with labels and separators, a user-avatar menu in a header, and a table-row kebab menu.

Basic actions menu with icons

The minimum viable Dropdown is a Trigger plus a Content with two or three Items. Pass a Lucide icon component to the icon prop on each Dropdown.Item and the component renders it at w-4 h-4 next to the label. Set destructive on the last item to apply the text-destructive hover:bg-destructive/10 utilities defined in styles.destructive on the registry source. The menu closes automatically after the click thanks to the setOpen(false) call inside Dropdown.Item's onClick wrapper.

Because the Dropdown source lives in your repo after install, swapping the icon size, the gap between icon and label, or the destructive color is a local edit. The trigger ships with Button styling out of the box — variant="outline" and rounded="md" are wired inside the Trigger function — so the call site stays a single line per item.

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

Right-aligned menu

When the trigger sits near the right edge of a container — a card header, a table cell, a sticky action bar — the menu should open to the left of the trigger so it does not clip off-screen. Pass align="right" on the Dropdown root and the styles.align.right utility (right-0 instead of left-0) anchors the content to the right side of the trigger. The vertical position is handled by absolute top-full mt-1 on styles.content regardless of which side you pick.

The align prop has two values: left (default) and right. There is no top, bottom, or auto-flip logic — for menus that need to flip around viewport edges, reach for a positioning library through Popover instead. For the common case of a left or right column action menu, the static align choice is the simpler shape and ships zero JavaScript for positioning.

1import { Dropdown } from "@/components/ui/dropdown"
2import { Settings, LogOut } from "lucide-react"
3
4export default function Page() {
5 return (
6 <div className="flex justify-end p-6">
7 <Dropdown align="right">
8 <Dropdown.Trigger>Account</Dropdown.Trigger>
9 <Dropdown.Content>
10 <Dropdown.Item icon={Settings}>Settings</Dropdown.Item>
11 <Dropdown.Separator />
12 <Dropdown.Item icon={LogOut} destructive>
13 Sign out
14 </Dropdown.Item>
15 </Dropdown.Content>
16 </Dropdown>
17 </div>
18 )
19}

Grouped items with labels and separators

For menus with more than four or five actions, group related items together. Dropdown.Group wraps a section with a py-1 rhythm, Dropdown.Label renders a muted heading at px-3 py-1.5 text-xs font-medium text-muted-foreground, and Dropdown.Separator draws a 1px horizontal line via my-1 h-px bg-border. The pattern matches how Gmail, Linear, and Notion structure their context menus — a label sets context, the items underneath act on that context, a separator breaks to the next group.

Keep groups short — three to five items max — and label each one with one or two words. If a group grows beyond five items, the menu is doing too much; split the actions into a Tabs or Sidebar instead. The Dropdown.Group element exists for visual rhythm only; it has no semantic role attached.

1import { Dropdown } from "@/components/ui/dropdown"
2import { Edit, Copy, Share2, Archive, Trash2 } from "lucide-react"
3
4export default function Page() {
5 return (
6 <Dropdown>
7 <Dropdown.Trigger>Manage</Dropdown.Trigger>
8 <Dropdown.Content>
9 <Dropdown.Group>
10 <Dropdown.Label>Edit</Dropdown.Label>
11 <Dropdown.Item icon={Edit}>Rename</Dropdown.Item>
12 <Dropdown.Item icon={Copy}>Duplicate</Dropdown.Item>
13 </Dropdown.Group>
14 <Dropdown.Separator />
15 <Dropdown.Group>
16 <Dropdown.Label>Share</Dropdown.Label>
17 <Dropdown.Item icon={Share2}>Share link</Dropdown.Item>
18 </Dropdown.Group>
19 <Dropdown.Separator />
20 <Dropdown.Group>
21 <Dropdown.Label>Danger zone</Dropdown.Label>
22 <Dropdown.Item icon={Archive}>Archive</Dropdown.Item>
23 <Dropdown.Item icon={Trash2} destructive>
24 Delete
25 </Dropdown.Item>
26 </Dropdown.Group>
27 </Dropdown.Content>
28 </Dropdown>
29 )
30}

User avatar menu in a header

The avatar menu is the standard pattern for the top-right corner of an app header. The trigger shows the user's avatar, opening reveals a small profile block (name, email), then a list of account actions. Pass an Avatar into Dropdown.Trigger as the children — the trigger Button renders it inside the outline style — and place the profile block as a non-interactive Dropdown.Group with a Dropdown.Label at the top of the content.

Use align="right" because the trigger sits at the right edge of the header. Keep the menu short — Profile, Settings, Theme, Sign out — and put the destructive action last with the destructive prop. The whole pattern fits in around 30 lines and ships zero positioning JavaScript.

1import { Dropdown } from "@/components/ui/dropdown"
2import { Avatar } from "@/components/ui/avatar"
3import { User, Settings, Moon, LogOut } from "lucide-react"
4
5export default function UserMenu() {
6 return (
7 <Dropdown align="right">
8 <Dropdown.Trigger>
9 <Avatar src="/me.jpg" alt="Ada" />
10 </Dropdown.Trigger>
11 <Dropdown.Content className="min-w-[220px]">
12 <Dropdown.Group>
13 <Dropdown.Label>Ada Lovelace</Dropdown.Label>
14 <div className="px-3 pb-2 text-xs text-muted-foreground">
15 ada@example.com
16 </div>
17 </Dropdown.Group>
18 <Dropdown.Separator />
19 <Dropdown.Item icon={User}>Profile</Dropdown.Item>
20 <Dropdown.Item icon={Settings}>Settings</Dropdown.Item>
21 <Dropdown.Item icon={Moon}>Toggle theme</Dropdown.Item>
22 <Dropdown.Separator />
23 <Dropdown.Item icon={LogOut} destructive>
24 Sign out
25 </Dropdown.Item>
26 </Dropdown.Content>
27 </Dropdown>
28 )
29}

Table-row kebab menu

Every row in a data table needs a place for row-scoped actions. The kebab — a vertical MoreVertical icon — is the canonical trigger because it stays out of the way until the user reaches for it. Drop the kebab into Dropdown.Trigger, set align="right" because the trigger sits in the last column, and wire each Dropdown.Item's onClick to a handler that captures the row id.

The trigger renders Button under the hood, so pass className to shrink the padding for a tight row height — h-8 w-8 p-0 puts the kebab in a square hit-area without an outline. For tables with hundreds of rows, the dropdown stays cheap: each row mounts its own state, no portal renders, and the outside-click listener attaches only after the trigger is clicked. See the Data Table docs for the full integration.

1import { Dropdown } from "@/components/ui/dropdown"
2import { MoreVertical, Eye, Edit, Trash2 } from "lucide-react"
3
4export function RowActions({ id }: { id: string }) {
5 return (
6 <Dropdown align="right">
7 <Dropdown.Trigger className="h-8 w-8 p-0">
8 <MoreVertical className="h-4 w-4" />
9 </Dropdown.Trigger>
10 <Dropdown.Content>
11 <Dropdown.Item icon={Eye} onClick={() => view(id)}>
12 View
13 </Dropdown.Item>
14 <Dropdown.Item icon={Edit} onClick={() => edit(id)}>
15 Edit
16 </Dropdown.Item>
17 <Dropdown.Separator />
18 <Dropdown.Item icon={Trash2} destructive onClick={() => remove(id)}>
19 Delete
20 </Dropdown.Item>
21 </Dropdown.Content>
22 </Dropdown>
23 )
24}
Get started

Install Drivn in one command

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

npx drivn@latest create

Requires Node 18+. Works with npm, pnpm, and yarn.

Enjoying Drivn?
Star the repo on GitHub to follow new component releases.
Star →

Frequently asked questions

You do not need to — Dropdown.Item wraps your onClick and calls setOpen(false) automatically after your handler runs. The wrapper lives inside the registry source: each Item button invokes the consumer's onClick, then closes the menu in the same click handler. If you need the menu to stay open after an item-click (rare, but useful for confirm-then-act flows), edit the local copy at @/components/ui/dropdown and remove the setOpen(false) line from the Item handler, or skip Dropdown.Item and render a plain <button> inside Dropdown.Content.

The trigger renders Drivn's Button component under the hood with variant="outline" and rounded="md" wired in. Whatever children you pass become the button label, so an icon, an avatar, or a mixed icon-plus-text label all work — you just give up the ability to render a non-button element at the trigger position. If you need the trigger to be a link, an avatar without the outline ring, or a custom element, edit the Trigger function inside @/components/ui/dropdown after install. The file lives in your repo, and the change is local to your project.

No — Dropdown.Item is a single flat action, and there is no Dropdown.Sub or Dropdown.SubContent in the registry source. The component is designed for short, click-driven menus where every action lives at the top level. If your menu needs hierarchical actions or a Files > Recent > Yesterday-style cascade, reach for shadcn/ui's Radix-backed DropdownMenu for that specific surface — see Drivn vs shadcn Dropdown for the comparison. The two libraries can coexist in the same project.

The dropdown content is positioned with absolute top-full mt-1 on styles.content and does not portal out of the DOM. That means an overflow: hidden ancestor can clip the menu when it opens. The fix is to give the ancestor overflow: visible for that one container, or move the dropdown outside the clipped subtree. If portaling is a hard requirement — long lists of items inside a virtualised table is the classic case — pick the Radix-backed DropdownMenu through shadcn/ui, which renders its content through a portal by default.

The Dropdown file starts with "use client" because it manages state with useState and attaches a mousedown listener through useEffect. You can import it from a Server Component, but it renders on the client. Next.js draws the client boundary correctly based on the directive at the top of the file — no manual configuration needed in your layout or page. Render the trigger and content inside any server-rendered page and the dropdown hydrates on the client automatically.