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 menu
1
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 found
2
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',
},
},
},
},
},
})