Dropdown Menu
Displays a menu to the user — such as a set of actions or functions — triggered by a button.
My Account
Profile⇧⌘P
Billing⌘B
Settings⌘S
Keyboard shortcuts⌘K
Team
Invite users
Email
Message
More...
New Team⌘+T
GitHub
Support
API
Log out⇧⌘Q
import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,import { css } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuSeparator,
DropdownMenuShortcut,
DropdownMenuSub,
DropdownMenuSubContent,
DropdownMenuSubTrigger,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
export function DropdownMenuDemo() {
return () => (
<DropdownMenu>
<DropdownMenuTrigger mix={buttonVariants({ variant: 'outline' })}>Open</DropdownMenuTrigger>
<DropdownMenuContent mix={w56} align="start">
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem>
Profile
<DropdownMenuShortcut>⇧⌘P</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Billing
<DropdownMenuShortcut>⌘B</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Settings
<DropdownMenuShortcut>⌘S</DropdownMenuShortcut>
</DropdownMenuItem>
<DropdownMenuItem>
Keyboard shortcuts
<DropdownMenuShortcut>⌘K</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuSub>
<DropdownMenuSubTrigger>Invite users</DropdownMenuSubTrigger>
<DropdownMenuPortal>
<DropdownMenuSubContent>
<DropdownMenuItem>Email</DropdownMenuItem>
<DropdownMenuItem>Message</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>More...</DropdownMenuItem>
</DropdownMenuSubContent>
</DropdownMenuPortal>
</DropdownMenuSub>
<DropdownMenuItem>
New Team
<DropdownMenuShortcut>⌘+T</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuItem>GitHub</DropdownMenuItem>
<DropdownMenuItem>Support</DropdownMenuItem>
<DropdownMenuItem disabled>API</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem>
Log out
<DropdownMenuShortcut>⇧⌘Q</DropdownMenuShortcut>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
const w56 = css({ width: '14rem' })Installation
$ bunx --bun shadcn@latest add https://www.shadcn-remix.com/r/dropdown-menu.json$ npx shadcn@latest add https://www.shadcn-remix.com/r/dropdown-menu.json$ pnpm dlx shadcn@latest add https://www.shadcn-remix.com/r/dropdown-menu.json$ yarn dlx shadcn@latest add https://www.shadcn-remix.com/r/dropdown-menu.jsonInstall the following dependencies:
$ bun add remix @remix-run/ui$ npm install remix @remix-run/ui$ pnpm add remix @remix-run/ui$ yarn add remix @remix-run/uiCopy and paste the following code into your project.
import { css, on, type Handle, type Props, type RemixNode } from 'remix/component'
import type { AnchorOptions } from '@remix-run/ui/anchor'
import * as menu from '@remix-run/ui/menu'
import * as popover from '@remix-run/ui/popover'
import { CheckIcon, ChevronRightIcon, CircleIcon } from './icons.tsx'
/** The parts of `@remix-run/ui/menu`'s context that the content components drive. */
interface MenuState {
readonly isOpen: boolean
open(options?: { strategy?: 'first' | 'last' | 'list' | 'none' }): Promise<void>
closeAll(): Promise<void>
}
export interface DropdownMenuProps {
/** Controlled open state. */
open?: boolean
/** Open on first render when uncontrolled. */
defaultOpen?: boolean
/** Called when the menu opens or closes. */
onOpenChange?: (open: boolean) => void
children?: RemixNode
}
interface OpenStateOwner {
readonly props: DropdownMenuProps
}
export function DropdownMenu(handle: Handle<DropdownMenuProps, OpenStateOwner>) {
handle.context.set({
get props() {
return handle.props
},
})
return () => <menu.Context>{handle.props.children}</menu.Context>
}
/** Radix portals menu content; here content renders in the top layer (`popover`) instead. */
export function DropdownMenuPortal(handle: Handle<{ children?: RemixNode }>) {
return () => handle.props.children
}
export function DropdownMenuTrigger(handle: Handle<Props<'button'>>) {
let dropdown = handle.context.get(menu.Context)
return () => {
let { mix, ...props } = handle.props
return (
<button
type="button"
data-slot="dropdown-menu-trigger"
data-state={dropdown.isOpen ? 'open' : 'closed'}
{...props}
mix={[menu.trigger(), mix]}
/>
)
}
}
/**
* Reports open changes of a menu to `onOpenChange` and applies `open`/`defaultOpen` prop changes.
* Call once per render of the menu's content.
*/
function createOpenStateSync(handle: Handle<object>, dropdown: MenuState) {
let reportedOpen = false
let appliedOpen: boolean | undefined
let initialized = false
return (props: DropdownMenuProps) => {
let isOpen = dropdown.isOpen
if (isOpen !== reportedOpen) {
reportedOpen = isOpen
handle.queueTask(() => props.onOpenChange?.(isOpen))
}
let requested = initialized ? props.open : (props.open ?? props.defaultOpen)
initialized = true
if (requested === undefined || requested === appliedOpen) return
appliedOpen = requested
if (requested === isOpen) return
handle.queueTask(() => {
if (requested) void dropdown.open({ strategy: 'list' })
else void dropdown.closeAll()
})
}
}
type Side = 'top' | 'right' | 'bottom' | 'left'
type Align = 'start' | 'center' | 'end'
/**
* Overrides the anchor options registered by the trigger with the content's Radix-style
* `side`/`align`/offsets right before the popover is positioned, and exposes the trigger size as
* `--radix-dropdown-menu-trigger-width|height`.
*/
function positionContent(
surface: popover.PopoverContext,
node: HTMLElement,
side: Side,
align: Align,
sideOffset: number,
alignOffset: number,
extra: AnchorOptions = {},
) {
let anchor = surface.anchor
if (!anchor) return
let alignShift = align === 'end' ? -alignOffset : alignOffset
let vertical = side === 'top' || side === 'bottom'
surface.anchor = {
target: anchor.target,
options: {
...anchor.options,
placement: align === 'center' ? side : `${side}-${align}`,
offset: sideOffset,
offsetX: vertical ? alignShift : 0,
offsetY: vertical ? 0 : alignShift,
...extra,
},
}
if (anchor.target instanceof HTMLElement) {
node.style.setProperty('--radix-dropdown-menu-trigger-width', `${anchor.target.offsetWidth}px`)
node.style.setProperty('--radix-dropdown-menu-trigger-height', `${anchor.target.offsetHeight}px`)
}
}
const surfaceAnimation = {
'&[data-state="open"]': {
animation: 'enter 150ms',
'--tw-enter-opacity': 0,
'--tw-enter-scale': 0.95,
},
// The popover leaves the top layer as soon as it is hidden; keep it rendered (display/overlay
// allow-discrete) for the exit animation unless the menu closes without animation.
'&[data-state="closed"]:not([data-close-animation="none"])': {
animation: 'exit 150ms',
animationFillMode: 'forwards',
'--tw-exit-opacity': 0,
'--tw-exit-scale': 0.95,
transition: 'display 150ms allow-discrete, overlay 150ms allow-discrete',
},
'&[data-anchor-placement^="bottom"]': { '--tw-enter-translate-y': '-0.5rem', transformOrigin: 'top' },
'&[data-anchor-placement^="top"]': { '--tw-enter-translate-y': '0.5rem', transformOrigin: 'bottom' },
'&[data-anchor-placement^="right"]': { '--tw-enter-translate-x': '-0.5rem', transformOrigin: 'left' },
'&[data-anchor-placement^="left"]': { '--tw-enter-translate-x': '0.5rem', transformOrigin: 'right' },
'&[data-anchor-placement="bottom-start"]': { transformOrigin: 'top left' },
'&[data-anchor-placement="bottom-end"]': { transformOrigin: 'top right' },
'&[data-anchor-placement="top-start"]': { transformOrigin: 'bottom left' },
'&[data-anchor-placement="top-end"]': { transformOrigin: 'bottom right' },
'&[data-anchor-placement="right-start"]': { transformOrigin: 'top left' },
'&[data-anchor-placement="right-end"]': { transformOrigin: 'bottom left' },
'&[data-anchor-placement="left-start"]': { transformOrigin: 'top right' },
'&[data-anchor-placement="left-end"]': { transformOrigin: 'bottom right' },
} as const
const contentStyles = css({
minWidth: '8rem',
overflowX: 'hidden',
overflowY: 'auto',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
backgroundColor: 'var(--popover)',
padding: '0.25rem',
color: 'var(--popover-foreground)',
boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)',
...surfaceAnimation,
})
const listStyles = css({ outlineStyle: 'none' })
export interface DropdownMenuContentProps extends Props<'div'> {
side?: Side
align?: Align
sideOffset?: number
alignOffset?: number
}
export function DropdownMenuContent(handle: Handle<DropdownMenuContentProps>) {
let dropdown = handle.context.get(menu.Context)
let surface = handle.context.get(popover.Context)
let owner = handle.context.get(DropdownMenu)
let syncOpenState = createOpenStateSync(handle, dropdown)
return () => {
let { side = 'bottom', align = 'center', sideOffset = 4, alignOffset = 0, mix, children, ...props } =
handle.props
syncOpenState(owner.props)
return (
<div
data-slot="dropdown-menu-content"
data-state={dropdown.isOpen ? 'open' : 'closed'}
{...props}
mix={[
contentStyles,
on('beforetoggle', (event) => {
if (event.newState !== 'open') return
positionContent(surface, event.currentTarget, side, align, sideOffset, alignOffset)
}),
menu.popover(),
mix,
]}
>
<div mix={[listStyles, menu.list()]}>{children}</div>
</div>
)
}
}
export function DropdownMenuGroup(handle: Handle<Props<'div'>>) {
return () => <div role="group" data-slot="dropdown-menu-group" {...handle.props} />
}
const itemStyles = css({
position: 'relative',
display: 'flex',
cursor: 'default',
alignItems: 'center',
gap: '0.5rem',
borderRadius: 'calc(var(--radius) - 4px)',
padding: '0.375rem 0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
outlineStyle: 'none',
userSelect: 'none',
'&[data-highlighted]': { backgroundColor: 'var(--accent)', color: 'var(--accent-foreground)' },
'&[data-disabled]': { pointerEvents: 'none', opacity: 0.5 },
'&[data-inset]': { paddingLeft: '2rem' },
'&[data-variant="destructive"]': { color: 'var(--destructive)' },
'&[data-variant="destructive"][data-highlighted]': {
backgroundColor: 'color-mix(in oklab, var(--destructive) 10%, transparent)',
color: 'var(--destructive)',
},
'.dark &[data-variant="destructive"][data-highlighted]': {
backgroundColor: 'color-mix(in oklab, var(--destructive) 20%, transparent)',
},
'& svg': { pointerEvents: 'none', flexShrink: 0 },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
'& svg:not([class*="text-"])': { color: 'var(--muted-foreground)' },
'&[data-variant="destructive"] > svg': { color: 'var(--destructive) !important' },
})
export interface DropdownMenuItemProps extends Props<'div'> {
inset?: boolean
variant?: 'default' | 'destructive'
disabled?: boolean
/** Text used for typeahead; defaults to the item's text content. */
textValue?: string
/** Called when the item is selected (click, Enter or Space). The menu then closes. */
onSelect?: (event: menu.MenuSelectEvent) => void
}
export function DropdownMenuItem(handle: Handle<DropdownMenuItemProps>) {
return () => {
let { inset, variant = 'default', disabled = false, textValue, onSelect, mix, ...props } = handle.props
return (
<div
data-slot="dropdown-menu-item"
data-inset={inset ? 'true' : undefined}
data-variant={variant}
data-disabled={disabled ? '' : undefined}
{...props}
mix={[
itemStyles,
menu.item({ name: handle.id, disabled, label: textValue }),
menu.onMenuSelect((event) => {
if (event.target === event.currentTarget) onSelect?.(event)
}),
mix,
]}
/>
)
}
}
const choiceItemStyles = css({
position: 'relative',
display: 'flex',
cursor: 'default',
alignItems: 'center',
gap: '0.5rem',
borderRadius: 'calc(var(--radius) - 4px)',
padding: '0.375rem 0.5rem 0.375rem 2rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
outlineStyle: 'none',
userSelect: 'none',
'&[data-highlighted]': { backgroundColor: 'var(--accent)', color: 'var(--accent-foreground)' },
'&[data-disabled]': { pointerEvents: 'none', opacity: 0.5 },
'& svg': { pointerEvents: 'none', flexShrink: 0 },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
})
const indicatorStyles = css({
pointerEvents: 'none',
position: 'absolute',
left: '0.5rem',
display: 'flex',
width: '0.875rem',
height: '0.875rem',
alignItems: 'center',
justifyContent: 'center',
})
const checkIconStyles = css({ width: '1rem', height: '1rem' })
const circleIconStyles = css({ width: '0.5rem', height: '0.5rem', fill: 'currentColor' })
export interface DropdownMenuCheckboxItemProps extends Props<'div'> {
checked?: boolean | 'indeterminate'
onCheckedChange?: (checked: boolean) => void
disabled?: boolean
textValue?: string
onSelect?: (event: menu.MenuSelectEvent) => void
}
export function DropdownMenuCheckboxItem(handle: Handle<DropdownMenuCheckboxItemProps>) {
return () => {
let { checked = false, onCheckedChange, disabled = false, textValue, onSelect, mix, children, ...props } =
handle.props
return (
<div
data-slot="dropdown-menu-checkbox-item"
data-state={checked === 'indeterminate' ? 'indeterminate' : checked ? 'checked' : 'unchecked'}
data-disabled={disabled ? '' : undefined}
{...props}
mix={[
choiceItemStyles,
menu.item({ name: handle.id, type: 'checkbox', checked: checked === true, disabled, label: textValue }),
menu.onMenuSelect((event) => {
if (event.target !== event.currentTarget) return
onSelect?.(event)
onCheckedChange?.(event.item.checked === true)
}),
mix,
]}
>
<span mix={indicatorStyles}>{checked !== false && <CheckIcon class="size-4" mix={checkIconStyles} />}</span>
{children}
</div>
)
}
}
export interface DropdownMenuRadioGroupProps extends Props<'div'> {
value?: string
onValueChange?: (value: string) => void
}
interface RadioGroupContext {
readonly name: string
readonly value: string | undefined
select(value: string): void
}
export function DropdownMenuRadioGroup(handle: Handle<DropdownMenuRadioGroupProps, RadioGroupContext>) {
handle.context.set({
name: handle.id,
get value() {
return handle.props.value
},
select(value) {
handle.props.onValueChange?.(value)
},
})
return () => {
let { value, onValueChange, ...props } = handle.props
return <div role="group" data-slot="dropdown-menu-radio-group" {...props} />
}
}
export interface DropdownMenuRadioItemProps extends Props<'div'> {
value: string
disabled?: boolean
textValue?: string
onSelect?: (event: menu.MenuSelectEvent) => void
}
export function DropdownMenuRadioItem(handle: Handle<DropdownMenuRadioItemProps>) {
let group = handle.context.get(DropdownMenuRadioGroup)
return () => {
let { value, disabled = false, textValue, onSelect, mix, children, ...props } = handle.props
let checked = group.value === value
return (
<div
data-slot="dropdown-menu-radio-item"
data-state={checked ? 'checked' : 'unchecked'}
data-disabled={disabled ? '' : undefined}
{...props}
mix={[
choiceItemStyles,
menu.item({ name: group.name, type: 'radio', value, checked, disabled, label: textValue }),
menu.onMenuSelect((event) => {
if (event.target !== event.currentTarget) return
onSelect?.(event)
group.select(value)
}),
mix,
]}
>
<span mix={indicatorStyles}>{checked && <CircleIcon class="size-2 fill-current" mix={circleIconStyles} />}</span>
{children}
</div>
)
}
}
const labelStyles = css({
padding: '0.375rem 0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
fontWeight: 500,
'&[data-inset]': { paddingLeft: '2rem' },
})
export interface DropdownMenuLabelProps extends Props<'div'> {
inset?: boolean
}
export function DropdownMenuLabel(handle: Handle<DropdownMenuLabelProps>) {
return () => {
let { inset, mix, ...props } = handle.props
return (
<div
data-slot="dropdown-menu-label"
data-inset={inset ? 'true' : undefined}
{...props}
mix={[labelStyles, mix]}
/>
)
}
}
const separatorStyles = css({
marginInline: '-0.25rem',
marginBlock: '0.25rem',
height: '1px',
backgroundColor: 'var(--border)',
})
export function DropdownMenuSeparator(handle: Handle<Props<'div'>>) {
return () => {
let { mix, ...props } = handle.props
return (
<div
role="separator"
aria-orientation="horizontal"
data-slot="dropdown-menu-separator"
{...props}
mix={[separatorStyles, mix]}
/>
)
}
}
const shortcutStyles = css({
marginLeft: 'auto',
fontSize: '0.75rem',
lineHeight: 'calc(1 / 0.75)',
letterSpacing: '0.1em',
color: 'var(--muted-foreground)',
})
export function DropdownMenuShortcut(handle: Handle<Props<'span'>>) {
return () => {
let { mix, ...props } = handle.props
return <span data-slot="dropdown-menu-shortcut" {...props} mix={[shortcutStyles, mix]} />
}
}
export function DropdownMenuSub(handle: Handle<DropdownMenuProps, OpenStateOwner>) {
handle.context.set({
get props() {
return handle.props
},
})
return () => <menu.Context>{handle.props.children}</menu.Context>
}
const subTriggerStyles = css({
display: 'flex',
cursor: 'default',
alignItems: 'center',
gap: '0.5rem',
borderRadius: 'calc(var(--radius) - 4px)',
padding: '0.375rem 0.5rem',
fontSize: '0.875rem',
lineHeight: 'calc(1.25 / 0.875)',
outlineStyle: 'none',
userSelect: 'none',
'&[data-highlighted], &[data-state="open"]': {
backgroundColor: 'var(--accent)',
color: 'var(--accent-foreground)',
},
'&[data-inset]': { paddingLeft: '2rem' },
'& svg': { pointerEvents: 'none', flexShrink: 0 },
'& svg:not([class*="size-"])': { width: '1rem', height: '1rem' },
'& svg:not([class*="text-"])': { color: 'var(--muted-foreground)' },
})
const chevronStyles = css({ marginLeft: 'auto', width: '1rem', height: '1rem' })
export interface DropdownMenuSubTriggerProps extends Props<'div'> {
inset?: boolean
disabled?: boolean
textValue?: string
}
export function DropdownMenuSubTrigger(handle: Handle<DropdownMenuSubTriggerProps>) {
let sub = handle.context.get(menu.Context)
return () => {
let { inset, disabled = false, textValue, mix, children, ...props } = handle.props
return (
<div
data-slot="dropdown-menu-sub-trigger"
data-inset={inset ? 'true' : undefined}
data-state={sub.isOpen ? 'open' : 'closed'}
data-disabled={disabled ? '' : undefined}
{...props}
mix={[subTriggerStyles, menu.submenuTrigger({ disabled, label: textValue }), mix]}
>
{children}
<ChevronRightIcon class="size-4" mix={chevronStyles} />
</div>
)
}
}
const subContentStyles = css({
minWidth: '8rem',
overflow: 'hidden',
borderRadius: 'calc(var(--radius) - 2px)',
borderWidth: '1px',
backgroundColor: 'var(--popover)',
padding: '0.25rem',
color: 'var(--popover-foreground)',
boxShadow: '0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)',
...surfaceAnimation,
})
export interface DropdownMenuSubContentProps extends Props<'div'> {
sideOffset?: number
alignOffset?: number
}
export function DropdownMenuSubContent(handle: Handle<DropdownMenuSubContentProps>) {
let sub = handle.context.get(menu.Context)
let surface = handle.context.get(popover.Context)
let owner = handle.context.get(DropdownMenuSub)
let syncOpenState = createOpenStateSync(handle, sub)
return () => {
let { sideOffset = 0, alignOffset = 0, mix, children, ...props } = handle.props
syncOpenState(owner.props)
return (
<div
data-slot="dropdown-menu-sub-content"
data-state={sub.isOpen ? 'open' : 'closed'}
{...props}
mix={[
subContentStyles,
on('beforetoggle', (event) => {
if (event.newState !== 'open') return
positionContent(surface, event.currentTarget, 'right', 'start', sideOffset, alignOffset, {
relativeTo: undefined,
})
}),
menu.popover(),
mix,
]}
>
<div mix={[listStyles, menu.list()]}>{children}</div>
</div>
)
}
}Update the import paths to match your project setup.
Usage
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'<DropdownMenu>
<DropdownMenuTrigger mix={buttonVariants({ variant: 'outline' })}>Open</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuGroup>
<DropdownMenuLabel>My Account</DropdownMenuLabel>
<DropdownMenuItem>Profile</DropdownMenuItem>
<DropdownMenuItem>Billing</DropdownMenuItem>
</DropdownMenuGroup>
<DropdownMenuSeparator />
<DropdownMenuGroup>
<DropdownMenuItem>Team</DropdownMenuItem>
<DropdownMenuItem>Subscription</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>Examples
Checkboxes
Use DropdownMenuCheckboxItem for toggles.
Appearance
Status Bar
Activity Bar
Panel
import { css, type Handle } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuCheckboxItem,import { css, type Handle } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuCheckboxItem,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
export function DropdownMenuCheckboxes(handle: Handle) {
let showStatusBar = true
let showActivityBar = false
let showPanel = false
return () => (
<DropdownMenu>
<DropdownMenuTrigger mix={buttonVariants({ variant: 'outline' })}>Open</DropdownMenuTrigger>
<DropdownMenuContent mix={w56}>
<DropdownMenuLabel>Appearance</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuCheckboxItem
checked={showStatusBar}
onCheckedChange={(checked) => {
showStatusBar = checked
handle.update()
}}
>
Status Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showActivityBar}
onCheckedChange={(checked) => {
showActivityBar = checked
handle.update()
}}
disabled
>
Activity Bar
</DropdownMenuCheckboxItem>
<DropdownMenuCheckboxItem
checked={showPanel}
onCheckedChange={(checked) => {
showPanel = checked
handle.update()
}}
>
Panel
</DropdownMenuCheckboxItem>
</DropdownMenuContent>
</DropdownMenu>
)
}
const w56 = css({ width: '14rem' })Radio Group
Use DropdownMenuRadioGroup for exclusive choices.
Panel Position
Top
Bottom
Right
import { css, type Handle } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,import { css, type Handle } from 'remix/component'
import { buttonVariants } from '#app/ui/button.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuLabel,
DropdownMenuRadioGroup,
DropdownMenuRadioItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
export function DropdownMenuRadioGroupExample(handle: Handle) {
let position = 'bottom'
return () => (
<DropdownMenu>
<DropdownMenuTrigger mix={buttonVariants({ variant: 'outline' })}>Open</DropdownMenuTrigger>
<DropdownMenuContent mix={w56}>
<DropdownMenuLabel>Panel Position</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
value={position}
onValueChange={(value) => {
position = value
handle.update()
}}
>
<DropdownMenuRadioItem value="top">Top</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="bottom">Bottom</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="right">Right</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</DropdownMenuContent>
</DropdownMenu>
)
}
const w56 = css({ width: '14rem' })Dialog
File Actions
New File...
Share...
Download
import { css, type Handle } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import {
Dialog,
DialogClose,import { css, type Handle } from 'remix/component'
import { Button, buttonVariants } from '#app/ui/button.tsx'
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
} from '#app/ui/dialog.tsx'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuGroup,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuTrigger,
} from '#app/ui/dropdown-menu.tsx'
import { Field, FieldGroup, FieldLabel } from '#app/ui/field.tsx'
import { MoreHorizontalIcon } from '#app/ui/icons.tsx'
import { Input } from '#app/ui/input.tsx'
import { Label } from '#app/ui/label.tsx'
import { Textarea } from '#app/ui/textarea.tsx'
const dialogWidth = css({ '@media (min-width: 40rem)': { maxWidth: '425px' } })
export function DropdownMenuDialog(handle: Handle) {
let showNewDialog = false
let showShareDialog = false
return () => (
<>
<DropdownMenu>
<DropdownMenuTrigger aria-label="Open menu" mix={buttonVariants({ variant: 'outline', size: 'icon-sm' })}>
<MoreHorizontalIcon />
</DropdownMenuTrigger>
<DropdownMenuContent mix={css({ width: '10rem' })} align="end">
<DropdownMenuLabel>File Actions</DropdownMenuLabel>
<DropdownMenuGroup>
<DropdownMenuItem
onSelect={() => {
showNewDialog = true
handle.update()
}}
>
New File...
</DropdownMenuItem>
<DropdownMenuItem
onSelect={() => {
showShareDialog = true
handle.update()
}}
>
Share...
</DropdownMenuItem>
<DropdownMenuItem disabled>Download</DropdownMenuItem>
</DropdownMenuGroup>
</DropdownMenuContent>
</DropdownMenu>
<Dialog
open={showNewDialog}
onOpenChange={(open) => {
showNewDialog = open
handle.update()
}}
>
<DialogContent mix={dialogWidth}>
<DialogHeader>
<DialogTitle>Create New File</DialogTitle>
<DialogDescription>
Provide a name for your new file. Click create when you're done.
</DialogDescription>
</DialogHeader>
<FieldGroup mix={css({ paddingBottom: '0.75rem' })}>
<Field>
<FieldLabel htmlFor="filename">File Name</FieldLabel>
<Input id="filename" name="filename" placeholder="document.txt" />
</Field>
</FieldGroup>
<DialogFooter>
<DialogClose mix={buttonVariants({ variant: 'outline' })}>Cancel</DialogClose>
<Button type="submit">Create</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<Dialog
open={showShareDialog}
onOpenChange={(open) => {
showShareDialog = open
handle.update()
}}
>
<DialogContent mix={dialogWidth}>
<DialogHeader>
<DialogTitle>Share File</DialogTitle>
<DialogDescription>Anyone with the link will be able to view this file.</DialogDescription>
</DialogHeader>
<FieldGroup mix={css({ paddingBlock: '0.75rem' })}>
<Field>
<Label htmlFor="email">Email Address</Label>
<Input id="email" name="email" type="email" placeholder="shadcn@vercel.com" autoComplete="off" />
</Field>
<Field>
<FieldLabel htmlFor="message">Message (Optional)</FieldLabel>
<Textarea id="message" name="message" placeholder="Check out this file" />
</Field>
</FieldGroup>
<DialogFooter>
<DialogClose mix={buttonVariants({ variant: 'outline' })}>Cancel</DialogClose>
<Button type="submit">Send Invite</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</>
)
}API Reference
DropdownMenu
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | - |
onOpenChange | ((open: boolean) => void) | - |
DropdownMenuPortal
Radix portals menu content; here content renders in the top layer (popover) instead.
DropdownMenuTrigger
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
DropdownMenuContent
| Prop | Type | Default |
|---|---|---|
side | Side | "bottom" |
align | Align | "center" |
sideOffset | number | 4 |
alignOffset | number | 0 |
mix | MixValue | - |
DropdownMenuGroup
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
DropdownMenuItem
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
variant | "default" | "destructive" | "default" |
disabled | boolean | false |
textValue | string | - |
onSelect | ((event: MenuSelectEvent) => void) | - |
mix | MixValue | - |
DropdownMenuCheckboxItem
| Prop | Type | Default |
|---|---|---|
checked | boolean | "indeterminate" | false |
onCheckedChange | ((checked: boolean) => void) | - |
disabled | boolean | false |
textValue | string | - |
onSelect | ((event: MenuSelectEvent) => void) | - |
mix | MixValue | - |
DropdownMenuRadioGroup
| Prop | Type | Default |
|---|---|---|
value | string | - |
onValueChange | ((value: string) => void) | - |
mix | MixValue | - |
DropdownMenuRadioItem
| Prop | Type | Default |
|---|---|---|
value | string | - |
disabled | boolean | false |
textValue | string | - |
onSelect | ((event: MenuSelectEvent) => void) | - |
mix | MixValue | - |
DropdownMenuLabel
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
mix | MixValue | - |
DropdownMenuSeparator
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
DropdownMenuShortcut
| Prop | Type | Default |
|---|---|---|
mix | MixValue | - |
DropdownMenuSub
| Prop | Type | Default |
|---|---|---|
open | boolean | - |
defaultOpen | boolean | - |
onOpenChange | ((open: boolean) => void) | - |
DropdownMenuSubTrigger
| Prop | Type | Default |
|---|---|---|
inset | boolean | - |
disabled | boolean | false |
textValue | string | - |
mix | MixValue | - |
DropdownMenuSubContent
| Prop | Type | Default |
|---|---|---|
sideOffset | number | 0 |
alignOffset | number | 0 |
mix | MixValue | - |
On This Page
InstallationUsageExamplesCheckboxesRadio GroupDialogAPI ReferenceDropdownMenuDropdownMenuPortalDropdownMenuTriggerDropdownMenuContentDropdownMenuGroupDropdownMenuItemDropdownMenuCheckboxItemDropdownMenuRadioGroupDropdownMenuRadioItemDropdownMenuLabelDropdownMenuSeparatorDropdownMenuShortcutDropdownMenuSubDropdownMenuSubTriggerDropdownMenuSubContent