Component
Dropdown menu
A menu of actions, checkboxes and radio items.
stablev0.3.0 Chromium 103 ui-scale safe
Install
Copies the file into
components/ds/, with its registry dependencies and npm packages. See Installation for the one-time setup.bunx shadcn@3 add @fastrp/dropdown-menuExamples
Default
API
<DropdownMenu />Takes the props of what it wrapsRadix's dropdown, sounding `open` and `close`; an item's `select` swallows the `close` after it.
<DropdownMenuItem />| Prop | Type | Default | Notes |
|---|---|---|---|
| inset | boolean | — | |
| variant | "danger" | "default" | default |
<DropdownMenuLabel />| Prop | Type | Default | Notes |
|---|---|---|---|
| inset | boolean | — |
<DropdownMenuSubTrigger />| Prop | Type | Default | Notes |
|---|---|---|---|
| inset | boolean | — |
Dependencies
npm
- @radix-ui/react-dropdown-menu@^2.1.4
- lucide-react@^0.469.0
Registry
- @fastrp/ui-sounds
- @fastrp/utils
Source
components/ds/dropdown-menu.tsxShowHide
'use client';
import * as DropdownMenuPrimitive from '@radix-ui/react-dropdown-menu';
import { Check, ChevronRight, Circle } from 'lucide-react';
import type * as React from 'react';
import { useSoundedOpen, useUiSound } from '@/components/ds/ui-sounds';
import { cn } from '@/lib/utils';
// @fast-ds [email protected]
/**
* Radix's dropdown, sounding `open` and `close`; an item's `select` swallows the `close` after it.
*/
function DropdownMenu({
open,
defaultOpen,
onOpenChange,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Root>) {
return (
<DropdownMenuPrimitive.Root
{...props}
{...useSoundedOpen({ open, defaultOpen, onOpenChange })}
/>
);
}
const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger;
const DropdownMenuGroup = DropdownMenuPrimitive.Group;
const DropdownMenuPortal = DropdownMenuPrimitive.Portal;
const DropdownMenuSub = DropdownMenuPrimitive.Sub;
const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup;
const panelClassName =
'panel z-popover min-w-[10rem] overflow-hidden rounded-lg p-1 text-popover-foreground data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2';
/** The highlight landing on an item, by key or pointer, ticks `navigate`. */
function useNavigateOnFocus<E extends React.FocusEvent>(onFocus?: (event: E) => void) {
const sound = useUiSound();
return (event: E) => {
sound('navigate');
onFocus?.(event);
};
}
const itemClassName =
'relative flex cursor-default select-none items-center gap-2 rounded-md px-2.5 py-2 text-sm outline-none transition-colors duration-fast focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0';
function DropdownMenuContent({
className,
sideOffset = 6,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
return (
<DropdownMenuPrimitive.Portal>
<DropdownMenuPrimitive.Content
data-slot="dropdown-menu-content"
sideOffset={sideOffset}
className={cn(
panelClassName,
'max-h-[var(--radix-dropdown-menu-content-available-height)] origin-[var(--radix-dropdown-menu-content-transform-origin)] overflow-y-auto',
className,
)}
{...props}
/>
</DropdownMenuPrimitive.Portal>
);
}
function DropdownMenuItem({
className,
inset,
variant = 'default',
onFocus,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Item> & {
inset?: boolean;
variant?: 'default' | 'danger';
}) {
return (
<DropdownMenuPrimitive.Item
data-slot="dropdown-menu-item"
data-variant={variant}
data-sfx="select"
onFocus={useNavigateOnFocus(onFocus)}
className={cn(
itemClassName,
inset && 'pl-8',
variant === 'danger' && 'text-danger-text focus:bg-danger focus:text-danger-foreground',
className,
)}
{...props}
/>
);
}
function DropdownMenuCheckboxItem({
className,
children,
checked,
onFocus,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.CheckboxItem>) {
return (
<DropdownMenuPrimitive.CheckboxItem
data-slot="dropdown-menu-checkbox-item"
data-sfx="toggle"
onFocus={useNavigateOnFocus(onFocus)}
className={cn(itemClassName, 'pl-8', className)}
checked={checked}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center text-brand">
<DropdownMenuPrimitive.ItemIndicator>
<Check />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.CheckboxItem>
);
}
function DropdownMenuRadioItem({
className,
children,
onFocus,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.RadioItem>) {
return (
<DropdownMenuPrimitive.RadioItem
data-slot="dropdown-menu-radio-item"
data-sfx="select"
onFocus={useNavigateOnFocus(onFocus)}
className={cn(itemClassName, 'pl-8', className)}
{...props}
>
<span className="absolute left-2 flex size-4 items-center justify-center text-brand">
<DropdownMenuPrimitive.ItemIndicator>
<Circle className="!size-2 fill-current" />
</DropdownMenuPrimitive.ItemIndicator>
</span>
{children}
</DropdownMenuPrimitive.RadioItem>
);
}
function DropdownMenuLabel({
className,
inset,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Label> & { inset?: boolean }) {
return (
<DropdownMenuPrimitive.Label
data-slot="dropdown-menu-label"
className={cn(
'px-2 py-1.5 text-eyebrow uppercase text-muted-foreground',
inset && 'pl-8',
className,
)}
{...props}
/>
);
}
function DropdownMenuSeparator({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.Separator>) {
return (
<DropdownMenuPrimitive.Separator
data-slot="dropdown-menu-separator"
className={cn('-mx-1 my-1 h-px bg-foreground/10', className)}
{...props}
/>
);
}
function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'span'>) {
return (
<span
data-slot="dropdown-menu-shortcut"
className={cn('ml-auto font-display text-xs tracking-[0.06em] opacity-60', className)}
{...props}
/>
);
}
function DropdownMenuSubTrigger({
className,
inset,
children,
onFocus,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubTrigger> & { inset?: boolean }) {
return (
<DropdownMenuPrimitive.SubTrigger
data-slot="dropdown-menu-sub-trigger"
onFocus={useNavigateOnFocus(onFocus)}
className={cn(
itemClassName,
'data-[state=open]:bg-foreground/[0.08]',
inset && 'pl-8',
className,
)}
{...props}
>
{children}
<ChevronRight className="ml-auto" />
</DropdownMenuPrimitive.SubTrigger>
);
}
function DropdownMenuSubContent({
className,
...props
}: React.ComponentProps<typeof DropdownMenuPrimitive.SubContent>) {
return (
<DropdownMenuPrimitive.SubContent
data-slot="dropdown-menu-sub-content"
className={cn(
panelClassName,
'origin-[var(--radix-dropdown-menu-content-transform-origin)]',
className,
)}
{...props}
/>
);
}
export {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
};