Installation
npx @park-ui/cli add selectAdd Component
Copy the code snippet below into you components folder.
'use client'
import type { Assign, SelectRootProps } from '@ark-ui/react'
import { ark } from '@ark-ui/react/factory'
import { Select, useSelectItemContext } from '@ark-ui/react/select'
import { CheckIcon, ChevronsUpDownIcon } from 'lucide-react'
import { forwardRef, type RefAttributes } from 'react'
import { createStyleContext } from 'styled-system/jsx'
import { type SelectVariantProps, select } from 'styled-system/recipes'
import type { HTMLStyledProps } from 'styled-system/types'
const { withProvider, withContext } = createStyleContext(select)
type StyleProps = SelectVariantProps & HTMLStyledProps<'div'>
export type RootProps<T> = Assign<SelectRootProps<T>, StyleProps> & RefAttributes<HTMLDivElement>
export const Root = withProvider(Select.Root, 'root') as Select.RootComponent<StyleProps>
export const ClearTrigger = withContext(Select.ClearTrigger, 'clearTrigger')
export const Content = withContext(Select.Content, 'content')
export const Control = withContext(Select.Control, 'control')
export const IndicatorGroup = withContext(ark.div, 'indicatorGroup')
export const Item = withContext(Select.Item, 'item')
export const ItemGroup = withContext(Select.ItemGroup, 'itemGroup')
export const ItemGroupLabel = withContext(Select.ItemGroupLabel, 'itemGroupLabel')
export const ItemText = withContext(Select.ItemText, 'itemText')
export const Label = withContext(Select.Label, 'label')
export const List = withContext(Select.List, 'list')
export const Positioner = withContext(Select.Positioner, 'positioner')
export const Trigger = withContext(Select.Trigger, 'trigger')
export const ValueText = withContext(Select.ValueText, 'valueText')
export const Indicator = withContext(Select.Indicator, 'indicator', {
defaultProps: { children: <ChevronsUpDownIcon /> },
})
export const HiddenSelect = Select.HiddenSelect
export {
SelectContext as Context,
SelectItemContext as ItemContext,
type SelectValueChangeDetails as ValueChangeDetails,
} from '@ark-ui/react/select'
const StyledItemIndicator = withContext(Select.ItemIndicator, 'itemIndicator')
export const ItemIndicator = forwardRef<HTMLDivElement, HTMLStyledProps<'div'>>(
function ItemIndicator(props, ref) {
const item = useSelectItemContext()
return item.selected ? (
<StyledItemIndicator ref={ref} {...props}>
<CheckIcon />
</StyledItemIndicator>
) : (
<svg aria-hidden="true" focusable="false" />
)
},
)
Integrate Recipe
Integrate this recipe in to your Panda config.
import { selectAnatomy } from '@ark-ui/react/select'
import { defineSlotRecipe } from '@pandacss/dev'
export const select = defineSlotRecipe({
className: 'select',
slots: selectAnatomy.extendWith('indicatorGroup').keys(),
base: {
root: {
display: 'flex',
flexDirection: 'column',
gap: '1.5',
width: 'full',
},
content: {
background: 'gray.surface.bg',
borderRadius: 'l2',
boxShadow: 'md',
display: 'flex',
flexDirection: 'column',
maxH: 'min(var(--available-height), {sizes.96})',
minWidth: 'max(var(--reference-width), {sizes.40})',
outline: 0,
overflowY: 'auto',
zIndex: 'dropdown',
_open: {
animationStyle: 'slide-fade-in',
animationDuration: 'slow',
},
_closed: {
animationStyle: 'slide-fade-out',
animationDuration: 'fastest',
},
},
item: {
alignItems: 'center',
borderRadius: 'l1',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
userSelect: 'none',
_hover: {
background: 'gray.surface.bg.hover',
},
_highlighted: {
background: 'gray.surface.bg.hover',
},
_selected: {},
_disabled: {
layerStyle: 'disabled',
},
},
indicatorGroup: {
display: 'flex',
alignItems: 'center',
gap: '1',
pointerEvents: 'none',
},
indicator: {
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
color: { base: 'fg.subtle' },
},
itemGroupLabel: {
alignItems: 'flex-start',
color: 'fg.subtle',
display: 'flex',
flexDirection: 'column',
fontWeight: 'medium',
gap: '1px',
justifyContent: 'center',
_after: {
content: '""',
width: '100%',
height: '1px',
bg: 'gray.4',
},
},
itemIndicator: {
color: 'colorPalette.plain.fg',
},
label: {
fontWeight: 'medium',
userSelect: 'none',
textStyle: 'sm',
},
trigger: {
alignItems: 'center',
borderRadius: 'l2',
cursor: 'pointer',
display: 'flex',
justifyContent: 'space-between',
minWidth: '0',
outline: '0',
textAlign: 'start',
transition: 'common',
userSelect: 'none',
width: 'full',
_placeholderShown: {
color: 'fg.subtle',
},
_disabled: {
layerStyle: 'disabled',
},
},
valueText: {
overflow: 'hidden',
textOverflow: 'ellipsis',
whiteSpace: 'nowrap',
},
},
defaultVariants: {
size: 'md',
variant: 'outline',
},
variants: {
variant: {
outline: {
trigger: {
borderWidth: '1px',
borderColor: 'gray.outline.border',
focusVisibleRing: 'inside',
},
},
surface: {
trigger: {
bg: 'gray.surface.bg',
borderWidth: '1px',
borderColor: 'gray.surface.border',
focusVisibleRing: 'inside',
},
},
},
size: {
xs: {
content: { p: '1', gap: '0.5', textStyle: 'sm' },
item: { px: '1', minH: '8', gap: '2', _icon: { boxSize: '3.5' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '1', height: '8' },
trigger: { px: '2', h: '8', textStyle: 'sm', gap: '2', _icon: { boxSize: '3.5' } },
},
sm: {
content: { p: '1', gap: '0.5', textStyle: 'sm' },
item: { px: '1.5', minH: '9', gap: '2', _icon: { boxSize: '4' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '1.5', height: '9' },
trigger: { px: '2.5', h: '9', textStyle: 'sm', gap: '2', _icon: { boxSize: '4' } },
},
md: {
content: { p: '1', gap: '0.5', textStyle: 'md' },
item: { px: '2', minH: '10', gap: '2', _icon: { boxSize: '4' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '2', height: '10' },
trigger: { px: '3', h: '10', textStyle: 'md', gap: '2', _icon: { boxSize: '4' } },
},
lg: {
content: { p: '1', gap: '0.5', textStyle: 'md' },
item: { px: '2.5', minH: '11', gap: '2', _icon: { boxSize: '4.5' } },
itemGroup: { gap: '0.5' },
itemGroupLabel: { px: '2.5', height: '11' },
trigger: { px: '3.5', h: '11', textStyle: 'md', gap: '2', _icon: { boxSize: '4.5' } },
},
xl: {
content: { p: '1', gap: '1', textStyle: 'lg' },
item: { px: '3', minH: '12', gap: '3', _icon: { boxSize: '5' } },
itemGroup: { gap: '1' },
itemGroupLabel: { px: '3', height: '12' },
trigger: { px: '4', h: '12', textStyle: 'lg', gap: '3', _icon: { boxSize: '5' } },
},
},
},
})
Usage
import { Select } from '@/components/ui'
<Select.Root>
<Select.HiddenSelect />
<Select.Label />
<Select.Control>
<Select.Trigger>
<Select.ValueText />
</Select.Trigger>
<Select.IndicatorGroup>
<Select.Indicator />
<Select.ClearTrigger />
</Select.IndicatorGroup>
</Select.Control>
<Select.Positioner>
<Select.Content>
<Select.Item />
<Select.ItemGroup>
<Select.ItemGroupLabel />
<Select.Item />
</Select.ItemGroup>
</Select.Content>
</Select.Positioner>
</Select.Root>
Examples
Size
Use the size prop to change the size of the select component.
Option Group
Use the Select.ItemGroup component to group select options.
Props
Root
| Prop | Default | Type |
|---|---|---|
collection* | ListCollection<T>The collection of items | |
variant | 'outline' | 'outline' | 'surface' |
size | 'md' | 'xs' | 'sm' | 'md' | 'lg' | 'xl' |
closeOnSelect | true | booleanWhether the select should close after an item is selected |
composite | true | booleanWhether the select is a composed with other composite widgets like tabs or combobox |
lazyMount | false | booleanWhether to enable lazy mounting |
loopFocus | false | booleanWhether to loop the keyboard navigation through the options |
skipAnimationOnMount | false | booleanWhether to allow the initial presence animation. |
unmountOnExit | false | booleanWhether to unmount on exit. |
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
defaultHighlightedValue | stringThe initial value of the highlighted item when opened. Use when you don't need to control the highlighted value of the select. | |
defaultOpen | booleanWhether the select's open state is controlled by the user | |
defaultValue | string[]The initial default value of the select when rendered. Use when you don't need to control the value of the select. | |
deselectable | booleanWhether the value can be cleared by clicking the selected item. **Note:** this is only applicable for single selection | |
disabled | booleanWhether the select is disabled | |
form | stringThe associate form of the underlying select. | |
highlightedValue | stringThe controlled key of the highlighted item | |
id | stringThe unique identifier of the machine. | |
ids | Partial<{
root: string
content: string
control: string
trigger: string
clearTrigger: string
label: string
hiddenSelect: string
positioner: string
item: (id: string | number) => string
itemGroup: (id: string | number) => string
itemGroupLabel: (id: string | number) => string
}>The ids of the elements in the select. Useful for composition. | |
immediate | booleanWhether to synchronize the present change immediately or defer it to the next frame | |
invalid | booleanWhether the select is invalid | |
multiple | booleanWhether to allow multiple selection | |
name | stringThe `name` attribute of the underlying select. | |
onExitComplete | VoidFunctionFunction called when the animation ends in the closed state | |
onFocusOutside | (event: FocusOutsideEvent) => voidFunction called when the focus is moved outside the component | |
onHighlightChange | (details: HighlightChangeDetails<T>) => voidThe callback fired when the highlighted item changes. | |
onInteractOutside | (event: InteractOutsideEvent) => voidFunction called when an interaction happens outside the component | |
onOpenChange | (details: OpenChangeDetails) => voidFunction called when the popup is opened | |
onPointerDownOutside | (event: PointerDownOutsideEvent) => voidFunction called when the pointer is pressed down outside the component | |
onSelect | (details: SelectionDetails) => voidFunction called when an item is selected | |
onValueChange | (details: ValueChangeDetails<T>) => voidThe callback fired when the selected item changes. | |
open | booleanWhether the select menu is open | |
positioning | PositioningOptionsThe positioning options of the menu. | |
present | booleanWhether the node is present (controlled by the user) | |
readOnly | booleanWhether the select is read-only | |
required | booleanWhether the select is required | |
scrollToIndexFn | (details: ScrollToIndexDetails) => voidFunction to scroll to a specific index | |
value | string[]The controlled keys of the selected items |
Item
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
item | anyThe item to render | |
persistFocus | booleanWhether hovering outside should clear the highlighted state |
ValueText
| Prop | Default | Type |
|---|---|---|
asChild | booleanUse the provided child element as the default rendered element, combining their props and behavior. | |
placeholder | stringText to display when no value is selected. |