Dropdown
A menu of actions and links that opens from a button.
import { Dropdown, DropdownButton, DropdownItem, DropdownMenu } from '@/components/dropdown'
import { ChevronDownIcon } from '@heroicons/react/16/solid'
export default function Example({ invoice, onDuplicate }) {
return (
<Dropdown>
<DropdownButton outline>
Actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu>
<DropdownItem href={`/invoices/${invoice.id}`}>Open invoice</DropdownItem>
<DropdownItem href={`/invoices/${invoice.id}/edit`}>Edit line items</DropdownItem>
<DropdownItem onClick={() => onDuplicate(invoice)}>Duplicate</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}<script setup lang="ts">
import Dropdown from '@/components/dropdown.vue'
import DropdownButton from '@/components/dropdown-button.vue'
import DropdownItem from '@/components/dropdown-item.vue'
import DropdownMenu from '@/components/dropdown-menu.vue'
import { ChevronDownIcon } from '@heroicons/vue/16/solid'
defineProps<{ invoice: { id: string } }>()
const emit = defineEmits<{ duplicate: [] }>()
</script>
<template>
<Dropdown>
<DropdownButton outline>
Actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu>
<DropdownItem :href="`/invoices/${invoice.id}`">Open invoice</DropdownItem>
<DropdownItem :href="`/invoices/${invoice.id}/edit`">Edit line items</DropdownItem>
<DropdownItem @click="emit('duplicate')">Duplicate</DropdownItem>
</DropdownMenu>
</Dropdown>
</template>Installation
Copy these files into the folder where you keep your components. They import each other with relative paths, so the folder can live anywhere. The packages they rely on are listed on the Installation page.
'use client'
import * as Headless from '@headlessui/react'
import clsx from 'clsx'
import type React from 'react'
import { Button } from './button'
import { Link } from './link'
const subgrid = [
'supports-[grid-template-columns:subgrid]:grid-cols-[auto_1fr_1.5rem_0.5rem_auto]',
'supports-[grid-template-columns:subgrid]:grid',
]
const anchorSpacing = [
'[--anchor-padding:--spacing(1)] [--anchor-gap:--spacing(2)]',
'data-[anchor~=start]:[--anchor-offset:-6px] sm:data-[anchor~=start]:[--anchor-offset:-4px]',
'data-[anchor~=end]:[--anchor-offset:6px] sm:data-[anchor~=end]:[--anchor-offset:4px]',
]
const menu = [
anchorSpacing,
'isolate z-50 w-max rounded-xl p-1 overflow-y-auto',
'focus:outline-hidden outline-transparent outline',
'bg-white/75 shadow-lg ring-1 ring-zinc-950/10 backdrop-blur-xl dark:bg-zinc-800/75 dark:ring-white/10 dark:ring-inset',
subgrid,
'transition data-closed:data-leave:opacity-0 data-leave:ease-in data-leave:duration-100',
]
const item = [
'group col-span-full grid cursor-default grid-cols-[auto_1fr_1.5rem_0.5rem_auto] items-center rounded-lg px-3.5 py-2.5 supports-[grid-template-columns:subgrid]:grid-cols-subgrid focus:outline-hidden sm:px-3 sm:py-1.5',
'text-left text-base/6 text-zinc-950 sm:text-sm/6 dark:text-white',
'data-focus:bg-accent-500 data-focus:text-white data-disabled:opacity-50',
'forced-color-adjust-none forced-colors:text-[CanvasText] forced-colors:data-focus:bg-[Highlight] forced-colors:data-focus:text-[HighlightText] forced-colors:data-focus:*:data-[slot=icon]:text-[HighlightText]',
'*:data-[slot=icon]:size-5 sm:*:data-[slot=icon]:size-4',
'*:data-[slot=icon]:first:col-start-1 *:data-[slot=icon]:first:row-start-1 *:data-[slot=icon]:first:-ml-0.5 *:data-[slot=icon]:first:mr-2.5 sm:*:data-[slot=icon]:first:mr-2',
'*:data-[slot=icon]:first:text-zinc-500 data-focus:*:data-[slot=icon]:first:text-white dark:*:data-[slot=icon]:first:text-zinc-400 dark:data-focus:*:data-[slot=icon]:first:text-white',
'*:data-[slot=avatar]:-ml-1 *:data-[slot=avatar]:mr-2.5 *:data-[slot=avatar]:size-6 sm:*:data-[slot=avatar]:mr-2 sm:*:data-[slot=avatar]:size-5',
]
const dangerItem = [
'text-red-600! data-focus:bg-red-600/10! *:data-[slot=icon]:text-red-600/75!',
'dark:text-red-400! dark:data-focus:bg-red-400/10! dark:*:data-[slot=icon]:text-red-400/75!',
]
const onFocusedItem = 'group-data-focus:text-white forced-colors:group-data-focus:text-[HighlightText]'
type DropdownItemProps = { className?: string; danger?: boolean } & (
| ({ href?: never } & Omit<Headless.MenuItemProps<'button'>, 'as' | 'className'>)
| ({ href: string } & Omit<Headless.MenuItemProps<typeof Link>, 'as' | 'className'>)
)
export function Dropdown(props: Headless.MenuProps) {
return <Headless.Menu {...props} />
}
export function DropdownButton<T extends React.ElementType = typeof Button>({
as = Button,
...props
}: { className?: string } & Omit<Headless.MenuButtonProps<T>, 'className'>) {
return <Headless.MenuButton as={as} {...props} />
}
export function DropdownMenu({
anchor = 'bottom',
className,
...props
}: { className?: string } & Omit<Headless.MenuItemsProps, 'as' | 'className'>) {
return <Headless.MenuItems {...props} transition anchor={anchor} className={clsx(className, menu)} />
}
export function DropdownItem({ className, danger = false, ...props }: DropdownItemProps) {
let classes = clsx(className, item, danger && dangerItem)
if (typeof props.href === 'string') {
return <Headless.MenuItem as={Link} {...props} className={classes} />
}
return <Headless.MenuItem as="button" type="button" {...props} className={classes} />
}
export function DropdownHeader({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div {...props} className={clsx(className, 'col-span-5 px-3.5 pt-2.5 pb-1 sm:px-3')} />
}
export function DropdownSection({
className,
...props
}: { className?: string } & Omit<Headless.MenuSectionProps, 'as' | 'className'>) {
return <Headless.MenuSection {...props} className={clsx(className, 'col-span-full', subgrid)} />
}
export function DropdownHeading({
className,
...props
}: { className?: string } & Omit<Headless.MenuHeadingProps, 'as' | 'className'>) {
return (
<Headless.MenuHeading
{...props}
className={clsx(
className,
'col-span-full grid grid-cols-[1fr_auto] gap-x-12',
'pt-2 pb-1 px-3.5 sm:px-3',
'font-medium text-sm/5 sm:text-xs/5 text-zinc-500 dark:text-zinc-400'
)}
/>
)
}
export function DropdownDivider({
className,
...props
}: { className?: string } & Omit<Headless.MenuSeparatorProps, 'as' | 'className'>) {
return (
<Headless.MenuSeparator
{...props}
className={clsx(
className,
'col-span-full h-px border-0 my-1 mx-3.5 sm:mx-3',
'bg-zinc-950/5 dark:bg-white/10 forced-colors:bg-[CanvasText]'
)}
/>
)
}
export function DropdownLabel({ className, ...props }: React.ComponentPropsWithoutRef<'div'>) {
return <div data-slot="label" {...props} className={clsx(className, 'col-start-2 row-start-1')} />
}
export function DropdownDescription({
className,
...props
}: { className?: string } & Omit<Headless.DescriptionProps, 'as' | 'className'>) {
return (
<Headless.Description
data-slot="description"
{...props}
className={clsx(
className,
onFocusedItem,
'col-span-2 col-start-2 row-start-2 text-sm/5 text-zinc-500 sm:text-xs/5 dark:text-zinc-400'
)}
/>
)
}
export function DropdownShortcut({
keys,
className,
...props
}: { keys: string | string[]; className?: string } & Omit<Headless.DescriptionProps<'kbd'>, 'as' | 'className'>) {
let characters = Array.isArray(keys) ? keys : keys.split('')
return (
<Headless.Description as="kbd" {...props} className={clsx(className, 'flex justify-self-end row-start-1 col-start-5')}>
{characters.map((char, index) => (
<kbd
key={index}
className={clsx(
'min-w-[2ch] text-center font-sans text-zinc-400 capitalize',
onFocusedItem,
index > 0 && char.length > 1 && 'pl-1'
)}
>
{char}
</kbd>
))}
</Headless.Description>
)
}Also copy Button.
<script setup lang="ts">
import { DropdownMenuRoot } from 'reka-ui'
</script>
<template>
<DropdownMenuRoot>
<slot />
</DropdownMenuRoot>
</template>Also copy Button.
Component API
| Prop | Default | Description |
|---|---|---|
Dropdownbuilt on the Headless UI <Headless.Menu> component | ||
| This component does not expose any component-specific props. | ||
DropdownButtonbuilt on the Headless UI <Headless.MenuButton> component | ||
as | Button | Component rendered as the trigger, such as AvatarButton or NavbarItem. |
DropdownMenubuilt on the Headless UI <Headless.MenuItems> component | ||
anchor | bottom | Side of the trigger the menu opens on, optionally aligned to its start or end. |
DropdownItembuilt on the Headless UI <Headless.MenuItem> component | ||
href | - | Turns the item into a link rendered through your router. |
disabled | false | Keeps the item visible but skips it on click and keyboard focus. |
danger | false | Red text and icon, with a light red highlight instead of the accent fill. |
DropdownHeaderrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
DropdownSectionbuilt on the Headless UI <Headless.MenuSection> component | ||
| This component does not expose any component-specific props. | ||
DropdownHeadingbuilt on the Headless UI <Headless.MenuHeading> component | ||
| This component does not expose any component-specific props. | ||
DropdownDividerbuilt on the Headless UI <Headless.MenuSeparator> component | ||
| This component does not expose any component-specific props. | ||
DropdownLabelrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
DropdownDescriptionbuilt on the Headless UI <Headless.Description> component | ||
| This component does not expose any component-specific props. | ||
DropdownShortcutrenders a <kbd> element | ||
keys | - | Keys to show, as a string split per character or an array of labels. |
| Prop | Default | Description |
|---|---|---|
Dropdownbuilt on the Reka UI <DropdownMenuRoot> component | ||
| This component does not expose any component-specific props. | ||
DropdownButtonbuilt on the <Button> component | ||
as | Button | Component rendered as the trigger, such as AvatarButton or NavbarItem. |
DropdownMenubuilt on the Reka UI <DropdownMenuContent> component | ||
anchor | bottom | Side of the trigger the menu opens on, optionally aligned to its start or end. |
DropdownItembuilt on the Reka UI <DropdownMenuItem> component | ||
href | - | Turns the item into a link rendered through your router. |
disabled | false | Keeps the item visible but skips it on click and keyboard focus. |
danger | false | Red text and icon, with a light red highlight instead of the accent fill. |
DropdownHeaderrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
DropdownSectionbuilt on the Reka UI <DropdownMenuGroup> component | ||
| This component does not expose any component-specific props. | ||
DropdownHeadingbuilt on the Reka UI <DropdownMenuLabel> component | ||
| This component does not expose any component-specific props. | ||
DropdownDividerbuilt on the Reka UI <DropdownMenuSeparator> component | ||
| This component does not expose any component-specific props. | ||
DropdownLabelrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
DropdownDescriptionrenders a <div> element | ||
| This component does not expose any component-specific props. | ||
DropdownShortcutrenders a <kbd> element | ||
keys | - | Keys to show, as a string split per character or an array of labels. |
Usage
A menu hides its items until someone asks for them, so keep it for secondary actions. The action people take most on a screen deserves its own button; a menu with only one or two items is usually better as buttons too.
Actions for a record
An item with href renders a link and one without renders a button, so navigation and actions can share the menu. Handle the action with onClick:
An item with href renders a link and one without renders a button, so navigation and actions can share the menu. Handle the action with @click:
import { Dropdown, DropdownButton, DropdownItem, DropdownMenu } from '@/components/dropdown'
import { ChevronDownIcon } from '@heroicons/react/16/solid'
export default function Example({ invoice, onDuplicate }) {
return (
<Dropdown>
<DropdownButton outline>
Actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu>
<DropdownItem href={`/invoices/${invoice.id}`}>Open invoice</DropdownItem>
<DropdownItem href={`/invoices/${invoice.id}/edit`}>Edit line items</DropdownItem>
<DropdownItem onClick={() => onDuplicate(invoice)}>Duplicate</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}<script setup lang="ts">
import Dropdown from '@/components/dropdown.vue'
import DropdownButton from '@/components/dropdown-button.vue'
import DropdownItem from '@/components/dropdown-item.vue'
import DropdownMenu from '@/components/dropdown-menu.vue'
import { ChevronDownIcon } from '@heroicons/vue/16/solid'
defineProps<{ invoice: { id: string } }>()
const emit = defineEmits<{ duplicate: [] }>()
</script>
<template>
<Dropdown>
<DropdownButton outline>
Actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu>
<DropdownItem :href="`/invoices/${invoice.id}`">Open invoice</DropdownItem>
<DropdownItem :href="`/invoices/${invoice.id}/edit`">Edit line items</DropdownItem>
<DropdownItem @click="emit('duplicate')">Duplicate</DropdownItem>
</DropdownMenu>
</Dropdown>
</template>Triggers
Create menu
DropdownButton renders a Button, so it takes color, outline and plain too. A solid trigger fits a menu that holds the main action of the page, like choosing what to create:
<Dropdown>
<DropdownButton>
<PlusIcon />
New
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem href="/invoices/new">Invoice</DropdownItem>
<DropdownItem href="/customers/new">Customer</DropdownItem>
<DropdownItem href="/payment-links/new">Payment link</DropdownItem>
</DropdownMenu>
</Dropdown><Dropdown>
<DropdownButton>
<PlusIcon />
New
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem href="/invoices/new">Invoice</DropdownItem>
<DropdownItem href="/customers/new">Customer</DropdownItem>
<DropdownItem href="/payment-links/new">Payment link</DropdownItem>
</DropdownMenu>
</Dropdown>Overflow button
In table rows and cards, a plain trigger with a single icon takes little room. Its aria-label should name the record, since every row has the same icon:
<Dropdown>
<DropdownButton plain aria-label="More actions for INV-2041">
<EllipsisHorizontalIcon />
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem>Download PDF</DropdownItem>
<DropdownItem>Send reminder</DropdownItem>
<DropdownItem>Mark as paid</DropdownItem>
</DropdownMenu>
</Dropdown><Dropdown>
<DropdownButton plain aria-label="More actions for INV-2041">
<EllipsisHorizontalIcon />
</DropdownButton>
<DropdownMenu anchor="bottom end">
<DropdownItem>Download PDF</DropdownItem>
<DropdownItem>Send reminder</DropdownItem>
<DropdownItem>Mark as paid</DropdownItem>
</DropdownMenu>
</Dropdown>Account menu
Pass another component to as to change what the trigger renders. AvatarButton gives the usual account menu in the top corner of an app:
'use client'
import { AvatarButton } from '@/components/avatar'
import { Dropdown, DropdownButton, DropdownDivider, DropdownItem, DropdownMenu } from '@/components/dropdown'
export function AccountMenu({ user, onSignOut }) {
return (
<Dropdown>
<DropdownButton as={AvatarButton} src={user.avatarUrl} className="size-8" aria-label="Account menu" />
<DropdownMenu anchor="bottom end">
<DropdownItem href="/profile">Profile</DropdownItem>
<DropdownItem href="/settings">Workspace settings</DropdownItem>
<DropdownDivider />
<DropdownItem onClick={onSignOut}>Sign out</DropdownItem>
</DropdownMenu>
</Dropdown>
)
}<script setup lang="ts">
import AvatarButton from '@/components/avatar-button.vue'
import Dropdown from '@/components/dropdown.vue'
import DropdownButton from '@/components/dropdown-button.vue'
import DropdownDivider from '@/components/dropdown-divider.vue'
import DropdownItem from '@/components/dropdown-item.vue'
import DropdownMenu from '@/components/dropdown-menu.vue'
defineProps<{ user: { avatarUrl: string } }>()
const emit = defineEmits<{ signOut: [] }>()
</script>
<template>
<Dropdown>
<DropdownButton class="size-8" :as="AvatarButton" :src="user.avatarUrl" aria-label="Account menu" />
<DropdownMenu anchor="bottom end">
<DropdownItem href="/profile">Profile</DropdownItem>
<DropdownItem href="/settings">Workspace settings</DropdownItem>
<DropdownDivider />
<DropdownItem @click="emit('signOut')">Sign out</DropdownItem>
</DropdownMenu>
</Dropdown>
</template>Workspace switcher
For a trigger that looks nothing like a button, render a plain element with as="button" and style it yourself. min-w-(--button-width) keeps the menu at least as wide as the trigger:
For a trigger that looks nothing like a button, use DropdownMenuTrigger from Reka UI and style it yourself. min-w-(--reka-dropdown-menu-trigger-width) keeps the menu at least as wide as the trigger:
<Dropdown>
<DropdownButton
as="button"
className="flex w-56 items-center gap-3 rounded-xl border border-transparent p-1 text-left data-active:border-zinc-200 data-hover:border-zinc-200 dark:data-active:border-zinc-700 dark:data-hover:border-zinc-700"
>
<Avatar square initials="NW" className="size-9 bg-zinc-900 text-white dark:bg-white dark:text-zinc-900" />
<span className="min-w-0 flex-1">
<span className="block text-sm/5 font-medium text-zinc-950 dark:text-white">Northwind</span>
<span className="block text-xs/5 text-zinc-500 dark:text-zinc-400">Scale plan</span>
</span>
<ChevronUpDownIcon className="mr-1 size-4 shrink-0 fill-zinc-400" />
</DropdownButton>
<DropdownMenu anchor="bottom start" className="min-w-(--button-width)">
<DropdownItem href="/w/northwind">Northwind</DropdownItem>
<DropdownItem href="/w/northwind-sandbox">Northwind Sandbox</DropdownItem>
<DropdownDivider />
<DropdownItem href="/workspaces/new">New workspace</DropdownItem>
</DropdownMenu>
</Dropdown><script setup lang="ts">
import Avatar from '@/components/avatar.vue'
import Dropdown from '@/components/dropdown.vue'
import DropdownDivider from '@/components/dropdown-divider.vue'
import DropdownItem from '@/components/dropdown-item.vue'
import DropdownMenu from '@/components/dropdown-menu.vue'
import { ChevronUpDownIcon } from '@heroicons/vue/16/solid'
import { DropdownMenuTrigger } from 'reka-ui'
</script>
<template>
<Dropdown>
<DropdownMenuTrigger
class="flex w-56 items-center gap-3 rounded-xl border border-transparent p-1 text-left data-[state=open]:border-zinc-200 hover:border-zinc-200 dark:data-[state=open]:border-zinc-700 dark:hover:border-zinc-700"
>
<Avatar square initials="NW" class="size-9 bg-zinc-900 text-white dark:bg-white dark:text-zinc-900" />
<span class="min-w-0 flex-1">
<span class="block text-sm/5 font-medium text-zinc-950 dark:text-white">Northwind</span>
<span class="block text-xs/5 text-zinc-500 dark:text-zinc-400">Scale plan</span>
</span>
<ChevronUpDownIcon class="mr-1 size-4 shrink-0 fill-zinc-400" />
</DropdownMenuTrigger>
<DropdownMenu anchor="bottom start" class="min-w-(--reka-dropdown-menu-trigger-width)">
<DropdownItem href="/w/northwind">Northwind</DropdownItem>
<DropdownItem href="/w/northwind-sandbox">Northwind Sandbox</DropdownItem>
<DropdownDivider />
<DropdownItem href="/workspaces/new">New workspace</DropdownItem>
</DropdownMenu>
</Dropdown>
</template>Content
Start each label with a verb and keep it short. Icons, descriptions and shortcuts help in long menus, but a menu of three plain labels reads faster without them.
Grouped items
DropdownSection groups related items and DropdownDivider draws a line between groups. Add a DropdownHeading when the group needs a name, or an aria-label when it does not show one:
<DropdownMenu>
<DropdownSection aria-label="Download">
<DropdownItem>Download PDF</DropdownItem>
<DropdownItem>Download CSV</DropdownItem>
</DropdownSection>
<DropdownDivider />
<DropdownSection>
<DropdownHeading>Share with the customer</DropdownHeading>
<DropdownItem>Copy payment link</DropdownItem>
<DropdownItem>Email invoice</DropdownItem>
</DropdownSection>
</DropdownMenu><DropdownMenu>
<DropdownSection aria-label="Download">
<DropdownItem>Download PDF</DropdownItem>
<DropdownItem>Download CSV</DropdownItem>
</DropdownSection>
<DropdownDivider />
<DropdownSection>
<DropdownHeading>Share with the customer</DropdownHeading>
<DropdownItem>Copy payment link</DropdownItem>
<DropdownItem>Email invoice</DropdownItem>
</DropdownSection>
</DropdownMenu>Icons
Put a 16px icon first in the item and wrap the text in DropdownLabel. The icon lines up in its own column and turns white with the text when the item is highlighted:
<DropdownMenu>
<DropdownItem href="/plans/scale/edit">
<PencilSquareIcon />
<DropdownLabel>Edit plan</DropdownLabel>
</DropdownItem>
<DropdownItem>
<DocumentDuplicateIcon />
<DropdownLabel>Duplicate plan</DropdownLabel>
</DropdownItem>
<DropdownItem>
<ArchiveBoxIcon />
<DropdownLabel>Archive plan</DropdownLabel>
</DropdownItem>
</DropdownMenu><DropdownMenu>
<DropdownItem href="/plans/scale/edit">
<PencilSquareIcon />
<DropdownLabel>Edit plan</DropdownLabel>
</DropdownItem>
<DropdownItem>
<DocumentDuplicateIcon />
<DropdownLabel>Duplicate plan</DropdownLabel>
</DropdownItem>
<DropdownItem>
<ArchiveBoxIcon />
<DropdownLabel>Archive plan</DropdownLabel>
</DropdownItem>
</DropdownMenu>Explaining similar choices
When two items sound alike, a DropdownDescription under the label tells them apart before anyone clicks:
<DropdownMenu anchor="bottom start">
<DropdownItem>
<DropdownLabel>Retry failed deliveries</DropdownLabel>
<DropdownDescription>Only events your endpoint answered with an error.</DropdownDescription>
</DropdownItem>
<DropdownItem>
<DropdownLabel>Replay the last 24 hours</DropdownLabel>
<DropdownDescription>Every event, including the ones already delivered.</DropdownDescription>
</DropdownItem>
</DropdownMenu><DropdownMenu anchor="bottom start">
<DropdownItem>
<DropdownLabel>Retry failed deliveries</DropdownLabel>
<DropdownDescription>Only events your endpoint answered with an error.</DropdownDescription>
</DropdownItem>
<DropdownItem>
<DropdownLabel>Replay the last 24 hours</DropdownLabel>
<DropdownDescription>Every event, including the ones already delivered.</DropdownDescription>
</DropdownItem>
</DropdownMenu>Keyboard shortcuts
DropdownShortcut shows the keys aligned on the right, so people learn the shortcut while using the menu. It only displays them: bind the keys yourself. A string is split into one key per character; pass an array for multi-letter keys:
<DropdownMenu anchor="bottom start">
<DropdownItem>
<DropdownLabel>New invoice</DropdownLabel>
<DropdownShortcut keys="N" />
</DropdownItem>
<DropdownItem>
<DropdownLabel>Duplicate</DropdownLabel>
<DropdownShortcut keys="⌘D" />
</DropdownItem>
<DropdownItem>
<DropdownLabel>Copy invoice ID</DropdownLabel>
<DropdownShortcut keys="⇧⌘C" />
</DropdownItem>
</DropdownMenu><DropdownMenu anchor="bottom start">
<DropdownItem>
<DropdownLabel>New invoice</DropdownLabel>
<DropdownShortcut keys="N" />
</DropdownItem>
<DropdownItem>
<DropdownLabel>Duplicate</DropdownLabel>
<DropdownShortcut keys="⌘D" />
</DropdownItem>
<DropdownItem>
<DropdownLabel>Copy invoice ID</DropdownLabel>
<DropdownShortcut keys="⇧⌘C" />
</DropdownItem>
</DropdownMenu>Context at the top
DropdownHeader holds content that is not an item, such as the account and workspace the menu acts on. It is skipped by keyboard navigation:
<DropdownMenu anchor="bottom end">
<DropdownHeader>
<div className="pr-6">
<div className="text-xs text-zinc-500 dark:text-zinc-400">Signed in to northwind as</div>
<div className="text-sm/7 font-semibold text-zinc-800 dark:text-white">kwame@northwind.dev</div>
</div>
</DropdownHeader>
<DropdownDivider />
<DropdownItem href="/account">Account settings</DropdownItem>
<DropdownItem href="/billing">Billing</DropdownItem>
<DropdownItem>Sign out</DropdownItem>
</DropdownMenu><DropdownMenu anchor="bottom end">
<DropdownHeader>
<div class="pr-6">
<div class="text-xs text-zinc-500 dark:text-zinc-400">Signed in to northwind as</div>
<div class="text-sm/7 font-semibold text-zinc-800 dark:text-white">kwame@northwind.dev</div>
</div>
</DropdownHeader>
<DropdownDivider />
<DropdownItem href="/account">Account settings</DropdownItem>
<DropdownItem href="/billing">Billing</DropdownItem>
<DropdownItem>Sign out</DropdownItem>
</DropdownMenu>States
Destructive item
danger colors the item red and swaps the accent highlight for a light red one. Put it last, after a divider, and open a confirmation from it rather than acting straight away:
<DropdownMenu anchor="bottom end">
<DropdownItem>
<ArrowPathIcon />
<DropdownLabel>Roll key</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem danger onClick={() => setConfirmOpen(true)}>
<NoSymbolIcon />
<DropdownLabel>Revoke key</DropdownLabel>
</DropdownItem>
</DropdownMenu><DropdownMenu anchor="bottom end">
<DropdownItem>
<ArrowPathIcon />
<DropdownLabel>Roll key</DropdownLabel>
</DropdownItem>
<DropdownDivider />
<DropdownItem danger @click="confirmOpen = true">
<NoSymbolIcon />
<DropdownLabel>Revoke key</DropdownLabel>
</DropdownItem>
</DropdownMenu>Unavailable item
A disabled item stays in place, faded, so the menu keeps the same shape from one record to the next. Here a pending payment cannot be refunded yet:
<DropdownMenu>
<DropdownItem>Download receipt</DropdownItem>
<DropdownItem disabled>Issue refund</DropdownItem>
<DropdownItem>Copy payment ID</DropdownItem>
</DropdownMenu><DropdownMenu>
<DropdownItem>Download receipt</DropdownItem>
<DropdownItem disabled>Issue refund</DropdownItem>
<DropdownItem>Copy payment ID</DropdownItem>
</DropdownMenu>Unavailable trigger
Disable the whole trigger when none of its items can run, such as bulk actions before any row is selected:
<Dropdown>
<DropdownButton outline disabled={selected.length === 0}>
Bulk actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu>{/* ... */}</DropdownMenu>
</Dropdown><Dropdown>
<DropdownButton outline :disabled="selected.length === 0">
Bulk actions
<ChevronDownIcon />
</DropdownButton>
<DropdownMenu><!-- ... --></DropdownMenu>
</Dropdown>Customization
Opening upward
anchor sets the side of the trigger the menu opens on. A trigger at the bottom of a sidebar needs top start so the menu grows up and lines up with its left edge:
<Dropdown>
<DropdownButton plain>
<QuestionMarkCircleIcon />
Help
</DropdownButton>
<DropdownMenu anchor="top start">
<DropdownItem href="/docs">Documentation</DropdownItem>
<DropdownItem href="/status">API status</DropdownItem>
<DropdownItem href="/support">Contact support</DropdownItem>
</DropdownMenu>
</Dropdown><Dropdown>
<DropdownButton plain>
<QuestionMarkCircleIcon />
Help
</DropdownButton>
<DropdownMenu anchor="top start">
<DropdownItem href="/docs">Documentation</DropdownItem>
<DropdownItem href="/status">API status</DropdownItem>
<DropdownItem href="/support">Contact support</DropdownItem>
</DropdownMenu>
</Dropdown>The side is top, bottom, left or right, optionally followed by start or end for the alignment.
Minimum width
The menu is as wide as its longest item. A min-w-* class keeps short menus from looking cramped under a wide trigger:
<DropdownMenu className="min-w-48">
<DropdownItem>Newest first</DropdownItem>
<DropdownItem>Oldest first</DropdownItem>
<DropdownItem>Amount</DropdownItem>
</DropdownMenu><DropdownMenu class="min-w-48">
<DropdownItem>Newest first</DropdownItem>
<DropdownItem>Oldest first</DropdownItem>
<DropdownItem>Amount</DropdownItem>
</DropdownMenu>Accessibility
Menu semantics and keyboard handling come from Headless UI Menu in React and Reka UI DropdownMenu in Vue. In Windows high contrast mode, the highlighted item uses the system highlight colors.