Menu
A list of options that appears when a user interacts with a button.
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 { 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>
            <Menu.ItemGroupLabel>My Account</Menu.ItemGroupLabel>
            <Menu.Separator />
            <Menu.Item value="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 value="billing">
              <HStack gap="2">
                <CreditCardIcon /> Billing
              </HStack>
            </Menu.Item>
            <Menu.Item value="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 value="email">
                    <HStack gap="2">
                      <MailIcon /> Email
                    </HStack>
                  </Menu.Item>
                  <Menu.Item value="message">
                    <HStack gap="2">
                      <MessageSquareIcon /> Message
                    </HStack>
                  </Menu.Item>
                  <Menu.Separator />
                  <Menu.Item value="other">
                    <HStack gap="2">
                      <PlusCircleIcon />
                      More Options...
                    </HStack>
                  </Menu.Item>
                </Menu.Content>
              </Menu.Positioner>
            </Menu.Root>
            <Menu.Separator />
            <Menu.Item value="logout">
              <HStack gap="2">
                <LogOutIcon />
                Logout
              </HStack>
            </Menu.Item>
          </Menu.ItemGroup>
        </Menu.Content>
      </Menu.Positioner>
    </Menu.Root>
  )
}
import {
  ChevronRightIcon,
  CreditCardIcon,
  LogOutIcon,
  MailIcon,
  MessageSquareIcon,
  PlusCircleIcon,
  SettingsIcon,
  UserIcon,
  UserPlusIcon,
} from 'lucide-solid'
import { HStack } from 'styled-system/jsx'
import { Button } from '~/components/ui/button'
import { Menu } from '~/components/ui/menu'
import { Text } from '~/components/ui/text'
export const Demo = (props: Menu.RootProps) => {
  return (
    <Menu.Root {...props}>
      <Menu.Trigger
        asChild={(triggerProps) => (
          <Button {...triggerProps()} variant="outline" size={props.size}>
            Open Menu
          </Button>
        )}
      />
      <Menu.Positioner>
        <Menu.Content>
          <Menu.ItemGroup>
            <Menu.ItemGroupLabel>My Account</Menu.ItemGroupLabel>
            <Menu.Separator />
            <Menu.Item value="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 value="billing">
              <HStack gap="2">
                <CreditCardIcon /> Billing
              </HStack>
            </Menu.Item>
            <Menu.Item value="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 value="email">
                    <HStack gap="2">
                      <MailIcon /> Email
                    </HStack>
                  </Menu.Item>
                  <Menu.Item value="message">
                    <HStack gap="2">
                      <MessageSquareIcon /> Message
                    </HStack>
                  </Menu.Item>
                  <Menu.Separator />
                  <Menu.Item value="other">
                    <HStack gap="2">
                      <PlusCircleIcon />
                      More Options...
                    </HStack>
                  </Menu.Item>
                </Menu.Content>
              </Menu.Positioner>
            </Menu.Root>
            <Menu.Separator />
            <Menu.Item value="logout">
              <HStack gap="2">
                <LogOutIcon />
                Logout
              </HStack>
            </Menu.Item>
          </Menu.ItemGroup>
        </Menu.Content>
      </Menu.Positioner>
    </Menu.Root>
  )
}
Usage
import { Menu } from '~/components/ui/menu'Installation
npx @park-ui/cli components add menu1
Add Styled Primitive
Copy the code snippet below into ~/components/ui/styled/menu.tsx
'use client'
import type { Assign } from '@ark-ui/react'
import { Menu } from '@ark-ui/react/menu'
import { type MenuVariantProps, menu } from 'styled-system/recipes'
import type { ComponentProps, HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withRootProvider, withContext } = createStyleContext(menu)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>(
  Menu.RootProvider,
)
export type RootProps = ComponentProps<typeof Root>
export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root)
export const Arrow = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>
>(Menu.Arrow, 'arrow')
export const ArrowTip = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>
>(Menu.ArrowTip, 'arrowTip')
export const CheckboxItem = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>
>(Menu.CheckboxItem, 'item')
export const Content = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>
>(Menu.Content, 'content')
export const ContextTrigger = withContext<
  HTMLButtonElement,
  Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps>
>(Menu.ContextTrigger, 'contextTrigger')
export const Indicator = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>
>(Menu.Indicator, 'indicator')
export const ItemGroupLabel = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps>
>(Menu.ItemGroupLabel, 'itemGroupLabel')
export const ItemGroup = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>
>(Menu.ItemGroup, 'itemGroup')
export const ItemIndicator = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps>
>(Menu.ItemIndicator, 'itemIndicator')
export const Item = withContext<HTMLDivElement, Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>(
  Menu.Item,
  'item',
)
export const ItemText = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>
>(Menu.ItemText, 'itemText')
export const Positioner = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>
>(Menu.Positioner, 'positioner')
export const RadioItemGroup = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps>
>(Menu.RadioItemGroup, 'itemGroup')
export const RadioItem = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>
>(Menu.RadioItem, 'item')
export const Separator = withContext<
  HTMLHRElement,
  Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>
>(Menu.Separator, 'separator')
export const TriggerItem = withContext<
  HTMLDivElement,
  Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>
>(Menu.TriggerItem, 'triggerItem')
export const Trigger = withContext<
  HTMLButtonElement,
  Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>
>(Menu.Trigger, 'trigger')
export { MenuContext as Context } from '@ark-ui/react/menu'
import { type Assign, Menu } from '@ark-ui/solid'
import type { ComponentProps } from 'solid-js'
import { type MenuVariantProps, menu } from 'styled-system/recipes'
import type { HTMLStyledProps } from 'styled-system/types'
import { createStyleContext } from './utils/create-style-context'
const { withRootProvider, withContext } = createStyleContext(menu)
export type RootProviderProps = ComponentProps<typeof RootProvider>
export const RootProvider = withRootProvider<Assign<Menu.RootProviderProps, MenuVariantProps>>(
  Menu.RootProvider,
)
export type RootProps = ComponentProps<typeof Root>
export const Root = withRootProvider<Assign<Menu.RootProps, MenuVariantProps>>(Menu.Root)
export const Arrow = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowBaseProps>>(
  Menu.Arrow,
  'arrow',
)
export const ArrowTip = withContext<Assign<HTMLStyledProps<'div'>, Menu.ArrowTipBaseProps>>(
  Menu.ArrowTip,
  'arrowTip',
)
export const CheckboxItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.CheckboxItemBaseProps>>(
  Menu.CheckboxItem,
  'item',
)
export const Content = withContext<Assign<HTMLStyledProps<'div'>, Menu.ContentBaseProps>>(
  Menu.Content,
  'content',
)
export const ContextTrigger = withContext<
  Assign<HTMLStyledProps<'button'>, Menu.ContextTriggerBaseProps>
>(Menu.ContextTrigger, 'contextTrigger')
export const Indicator = withContext<Assign<HTMLStyledProps<'div'>, Menu.IndicatorBaseProps>>(
  Menu.Indicator,
  'indicator',
)
export const ItemGroupLabel = withContext<
  Assign<HTMLStyledProps<'div'>, Menu.ItemGroupLabelBaseProps>
>(Menu.ItemGroupLabel, 'itemGroupLabel')
export const ItemGroup = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemGroupBaseProps>>(
  Menu.ItemGroup,
  'itemGroup',
)
export const ItemIndicator = withContext<
  Assign<HTMLStyledProps<'div'>, Menu.ItemIndicatorBaseProps>
>(Menu.ItemIndicator, 'itemIndicator')
export const Item = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemBaseProps>>(
  Menu.Item,
  'item',
)
export const ItemText = withContext<Assign<HTMLStyledProps<'div'>, Menu.ItemTextBaseProps>>(
  Menu.ItemText,
  'itemText',
)
export const Positioner = withContext<Assign<HTMLStyledProps<'div'>, Menu.PositionerBaseProps>>(
  Menu.Positioner,
  'positioner',
)
export const RadioItemGroup = withContext<
  Assign<HTMLStyledProps<'div'>, Menu.RadioItemGroupBaseProps>
>(Menu.RadioItemGroup, 'itemGroup')
export const RadioItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.RadioItemBaseProps>>(
  Menu.RadioItem,
  'item',
)
export const Separator = withContext<Assign<HTMLStyledProps<'hr'>, Menu.SeparatorBaseProps>>(
  Menu.Separator,
  'separator',
)
export const TriggerItem = withContext<Assign<HTMLStyledProps<'div'>, Menu.TriggerItemBaseProps>>(
  Menu.TriggerItem,
  'triggerItem',
)
export const Trigger = withContext<Assign<HTMLStyledProps<'button'>, Menu.TriggerBaseProps>>(
  Menu.Trigger,
  'trigger',
)
export { MenuContext as Context } from '@ark-ui/solid'
No snippet found2
Add Re-Export
To improve the developer experience, re-export the styled primitives in~/components/ui/menu.tsx.
export * as Menu from './styled/menu'
export * as Menu from './styled/menu'
3
Integrate Recipe
If you're not using @park-ui/preset, add the following recipe to yourpanda.config.ts:
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',
  },
  _disabled: {
    color: 'fg.disabled',
    cursor: 'not-allowed',
    _hover: {
      color: 'fg.disabled',
      background: 'none',
    },
  },
}
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,
    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',
          },
        },
      },
    },
  },
})