Components
Menu

Menu

A list of options that appears when a user interacts with a button.

Usage

import {
  ChevronRightIcon,
  CreditCardIcon,
  LogOutIcon,
  MailIcon,
  MessageSquareIcon,
  PlusCircleIcon,
  SettingsIcon,
  UserIcon,
  UserPlusIcon,
} from 'lucide-react'
import { HStack } from 'styled-system/jsx'
import { Button } from '~/components/ui/button'
import * as Menu from '~/components/ui/menu'
import { Text } from '~/components/ui/text'

export const Demo = (props: Menu.RootProps) => {
  return (
    <Menu.Root {...props}>
      <Menu.Trigger asChild>
        <Button variant="outline" size={props.size}>
          Open Menu
        </Button>
      </Menu.Trigger>
      <Menu.Positioner>
        <Menu.Content>
          <Menu.ItemGroup id="group-1">
            <Menu.ItemGroupLabel htmlFor="group-1">My Account</Menu.ItemGroupLabel>
            <Menu.Separator />
            <Menu.Item id="profile">
              <HStack gap="6" justify="space-between" flex="1">
                <HStack gap="2">
                  <UserIcon />
                  Profile
                </HStack>
                <Text as="span" color="fg.subtle" size="sm">
                  ⇧⌘P
                </Text>
              </HStack>
            </Menu.Item>
            <Menu.Item id="billing">
              <HStack gap="2">
                <CreditCardIcon /> Billing
              </HStack>
            </Menu.Item>
            <Menu.Item id="settings">
              <HStack gap="6" justify="space-between" flex="1">
                <HStack gap="2">
                  <SettingsIcon /> Settings
                </HStack>
                <Text as="span" color="fg.subtle" size="sm">
                  ⌘,
                </Text>
              </HStack>
            </Menu.Item>
            <Menu.Root positioning={{ placement: 'right-start', gutter: -2 }} {...props}>
              <Menu.TriggerItem justifyContent="space-between">
                <HStack gap="2">
                  <UserPlusIcon />
                  Invite member
                </HStack>
                <ChevronRightIcon />
              </Menu.TriggerItem>
              <Menu.Positioner>
                <Menu.Content>
                  <Menu.Item id="email">
                    <HStack gap="2">
                      <MailIcon /> Email
                    </HStack>
                  </Menu.Item>
                  <Menu.Item id="message">
                    <HStack gap="2">
                      <MessageSquareIcon /> Message
                    </HStack>
                  </Menu.Item>
                  <Menu.Separator />
                  <Menu.Item id="other">
                    <HStack gap="2">
                      <PlusCircleIcon />
                      More Options...
                    </HStack>
                  </Menu.Item>
                </Menu.Content>
              </Menu.Positioner>
            </Menu.Root>
            <Menu.Separator />
            <Menu.Item id="logout">
              <HStack gap="2">
                <LogOutIcon />
                Logout
              </HStack>
            </Menu.Item>
          </Menu.ItemGroup>
        </Menu.Content>
      </Menu.Positioner>
    </Menu.Root>
  )
}

Installation

1

Add Component

Insert code snippet into your project. Update import paths as needed.

import { Menu } from '@ark-ui/react/menu'
import type { ComponentProps } from 'react'
import { styled } from 'styled-system/jsx'
import { menu } from 'styled-system/recipes'
import { createStyleContext } from '~/lib/create-style-context'

const { withProvider, withContext } = createStyleContext(menu)

export const Root = withProvider(Menu.Root)
export const Arrow = withContext(styled(Menu.Arrow), 'arrow')
export const ArrowTip = withContext(styled(Menu.ArrowTip), 'arrowTip')
export const Content = withContext(styled(Menu.Content), 'content')
export const ContextTrigger = withContext(styled(Menu.ContextTrigger), 'contextTrigger')
export const Item = withContext(styled(Menu.Item), 'item')
export const ItemGroup = withContext(styled(Menu.ItemGroup), 'itemGroup')
export const ItemGroupLabel = withContext(styled(Menu.ItemGroupLabel), 'itemGroupLabel')
export const OptionItem = withContext(styled(Menu.OptionItem), 'optionItem')
export const Positioner = withContext(styled(Menu.Positioner), 'positioner')
export const Separator = withContext(styled(Menu.Separator), 'separator')
export const Trigger = withContext(styled(Menu.Trigger), 'trigger')
export const TriggerItem = withContext(styled(Menu.TriggerItem), 'triggerItem')

export interface RootProps extends ComponentProps<typeof Root> {}
export interface ArrowProps extends ComponentProps<typeof Arrow> {}
export interface ArrowTipProps extends ComponentProps<typeof ArrowTip> {}
export interface ContentProps extends ComponentProps<typeof Content> {}
export interface ContextTriggerProps extends ComponentProps<typeof ContextTrigger> {}
export interface ItemProps extends ComponentProps<typeof Item> {}
export interface ItemGroupProps extends ComponentProps<typeof ItemGroup> {}
export interface ItemGroupLabelProps extends ComponentProps<typeof ItemGroupLabel> {}
export interface OptionItemProps extends ComponentProps<typeof OptionItem> {}
export interface PositionerProps extends ComponentProps<typeof Positioner> {}
export interface SeparatorProps extends ComponentProps<typeof Separator> {}
export interface TriggerProps extends ComponentProps<typeof Trigger> {}
export interface TriggerItemProps extends ComponentProps<typeof TriggerItem> {}
2

Add Recipe

This step is necessary only if you do not use any of the Park UI plugins.

import { menuAnatomy } from '@ark-ui/anatomy'
import { defineSlotRecipe } from '@pandacss/dev'

const itemStyle = {
  alignItems: 'center',
  borderRadius: 'l1',
  cursor: 'pointer',
  display: 'flex',
  fontWeight: 'medium',
  textStyle: 'sm',
  transitionDuration: 'fast',
  transitionProperty: 'background, color',
  transitionTimingFunction: 'default',
  _hover: {
    background: 'bg.muted',
    '& :where(svg)': {
      color: 'fg.default',
    },
  },
  _highlighted: {
    background: 'bg.muted',
  },
  '& :where(svg)': {
    color: 'fg.muted',
  },
}

export const menu = defineSlotRecipe({
  className: 'menu',
  slots: menuAnatomy.keys(),
  base: {
    itemGroupLabel: {
      fontWeight: 'semibold',
      textStyle: 'sm',
    },
    content: {
      background: 'bg.default',
      borderRadius: 'l2',
      boxShadow: 'lg',
      display: 'flex',
      flexDirection: 'column',
      outline: 'none',
      width: 'calc(100% + 2rem)',
      zIndex: 'dropdown',
      _hidden: {
        display: 'none',
      },
      _open: {
        animation: 'fadeIn 0.25s ease-out',
      },
      _closed: {
        animation: 'fadeOut 0.2s ease-out',
      },
    },
    itemGroup: {
      display: 'flex',
      flexDirection: 'column',
    },
    positioner: {
      zIndex: 'dropdown',
    },
    item: itemStyle,
    optionItem: itemStyle,
    triggerItem: itemStyle,
  },
  defaultVariants: {
    size: 'md',
  },
  variants: {
    size: {
      xs: {
        itemGroup: {
          gap: '1',
        },
        itemGroupLabel: {
          py: '1.5',
          px: '1.5',
          mx: '1',
        },
        content: {
          py: '1',
          gap: '1',
        },
        item: {
          h: '8',
          px: '1.5',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        optionItem: {
          h: '8',
          px: '1.5',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        triggerItem: {
          h: '8',
          px: '1.5',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
      },
      sm: {
        itemGroup: {
          gap: '1',
        },
        itemGroupLabel: {
          py: '2',
          px: '2',
          mx: '1',
        },
        content: {
          py: '1',
          gap: '1',
        },
        item: {
          h: '9',
          px: '2',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        optionItem: {
          h: '9',
          px: '2',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        triggerItem: {
          h: '9',
          px: '2',
          mx: '1.5',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
      },
      md: {
        itemGroup: {
          gap: '1',
        },
        itemGroupLabel: {
          py: '2.5',
          px: '2.5',
          mx: '1',
        },
        content: {
          py: '1',
          gap: '1',
        },
        item: {
          h: '10',
          px: '2.5',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        optionItem: {
          h: '10',
          px: '2.5',
          mx: '1',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
        triggerItem: {
          h: '10',
          px: '2.5',
          mx: '1.5',
          '& :where(svg)': {
            width: '4',
            height: '4',
          },
        },
      },
      lg: {
        itemGroup: {
          gap: '1',
        },
        itemGroupLabel: {
          py: '2.5',
          px: '2.5',
          mx: '1',
        },
        content: {
          py: '1',
          gap: '1',
        },
        item: {
          h: '11',
          px: '2.5',
          mx: '1',
          '& :where(svg)': {
            width: '5',
            height: '5',
          },
        },
        optionItem: {
          h: '11',
          px: '2.5',
          mx: '1',
          '& :where(svg)': {
            width: '5',
            height: '5',
          },
        },
        triggerItem: {
          h: '11',
          px: '2.5',
          mx: '1.5',
          '& :where(svg)': {
            width: '5',
            height: '5',
          },
        },
      },
    },
  },
})

On this page