SelectPanel
The SelectPanel is an anchored dialog that allows users to quickly navigate and select one or multiple items from a list. It includes a text input for filtering, supports item grouping, and offers a footer for additional actions. Changes are applied upon closing the panel.
Page navigation navigation
React examples
Single-select
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function SingleSelect() { const [selected, setSelected] = React.useState<ActionListItemInput | undefined>(items[0]) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => item.text === selected?.text || item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { if (a.text === selected?.text) return -1 if (b.text === selected?.text) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choice</FormControl.Label> <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick one choice" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
Multi-select
When users search for new items, maintain their current selections and use a minimal loading state to indicate ongoing activity.
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function MultiSelect() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choices</FormControl.Label> <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
Grouped items
Items can be grouped to provide additional context or to visually separate them. Each group can have a title for better organization.
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListGroupedListProps, type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {groupId: '0', text: 'Choice one'}, {groupId: '0', text: 'Choice two'}, {groupId: '0', text: 'Choice three'}, {groupId: '1', text: 'Listing one'}, {groupId: '1', text: 'Listing two'}, {groupId: '1', text: 'Listing three'}, {groupId: '2', text: 'Item one'}, {groupId: '2', text: 'Item two'}, {groupId: '2', text: 'Item three'}, ] const groupMetadata: ActionListGroupedListProps['groupMetadata'] = [ {groupId: '0', header: {title: 'Choices', variant: 'filled'}}, {groupId: '1', header: {title: 'Listings', variant: 'filled'}}, {groupId: '2', header: {title: 'Items', variant: 'filled'}}, ] export default function Groups() { const [selected, setSelected] = React.useState<ActionListItemInput[]>([items[2], items[5], items[8]]) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected item in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { if (a.groupId === b.groupId) { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 } return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Selections</FormControl.Label> <SelectPanel groupMetadata={groupMetadata} renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick stuff" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
Items with leading visuals
import React from 'react' import {Avatar, Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' ;<Avatar alt="Atom logo" src="https://avatars.githubusercontent.com/atom" /> const items: ActionListItemInput[] = [ { leadingVisual: () => <Avatar alt="GitHub logo" size={16} src="https://avatars.githubusercontent.com/github" />, text: 'GitHub', }, { leadingVisual: () => <Avatar alt="Primer logo" size={16} src="https://avatars.githubusercontent.com/primer" />, text: 'Primer', }, { leadingVisual: () => <Avatar alt="Atom logo" size={16} src="https://avatars.githubusercontent.com/atom" />, text: 'Atom', }, ] export default function LeadingVisual() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Software</FormControl.Label> <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick software" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
Items with dividers
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function Dividers() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choices</FormControl.Label> <SelectPanel showItemDividers renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
With header
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function Header() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choices</FormControl.Label> <SelectPanel title="Choice list" subtitle="Pick as many choices as you want." renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
With a footer
An optional footer at the bottom can include a link or button for additional actions.
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function Footer() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choices</FormControl.Label> <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} footer={ <Button size="small" block onClick={() => alert('fake edit mode toggle')}> Edit choices </Button> } /> </FormControl> ) }
Loading
Provide visual cues to users when processes may take longer than expected. Use loading states to communicate results are loading. Use when retrieving initial data to prevent users from seeing an empty list.
import React from 'react' import {Button, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function Loading() { const [selected, setSelected] = React.useState<ActionListItemInput | undefined>(items[0]) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => item.text === selected?.text || item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { if (a.text === selected?.text) return -1 if (b.text === selected?.text) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <SelectPanel loading={true} title="Select labels" subtitle="Use labels to organize issues and pull requests" renderAnchor={({children, 'aria-labelledby': ariaLabelledBy, ...anchorProps}) => ( <Button trailingAction={TriangleDownIcon} aria-labelledby={` ${ariaLabelledBy}`} {...anchorProps} aria-haspopup="dialog" > {children ?? 'Select Labels'} </Button> )} placeholderText="Filter labels" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> ) }
Other label options
If the button represents the current selection, it must have an associated label, either internally (within the button) or externally (adjacent to the button).
Visually hidden label
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function LabelHidden() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label visuallyHidden>Choices</FormControl.Label> <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> {children} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }
Internal label
import React from 'react' import {Box, Button, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function LabelInternal() { const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <SelectPanel renderAnchor={({children, ...anchorProps}) => ( <Button {...anchorProps} trailingAction={TriangleDownIcon} aria-haspopup="dialog"> <Box sx={{ color: 'var(--fgColor-muted)', display: 'inline-block', }} > Choices: </Box>{' '} {children || 'None selected'} </Button> )} placeholder="Pick choices" open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> ) }
Custom (external) anchor
To use an external anchor, pass an anchorRef
to SelectPanel
. You will also need to manually toggle the open
prop when activating the custom anchor.
import React from 'react' import {Button, FormControl, SelectPanel} from '@primer/react' import {TriangleDownIcon} from '@primer/octicons-react' import {type ActionListItemInput} from '@primer/react/deprecated' const items: ActionListItemInput[] = [ {text: 'Choice one'}, {text: 'Choice two'}, {text: 'Choice three'}, {text: 'Choice four'}, {text: 'Choice five'}, ] export default function MultiSelect() { const buttonRef = React.useRef<HTMLButtonElement>(null) const [selected, setSelected] = React.useState<ActionListItemInput[]>(items.slice(1, 3)) const [filter, setFilter] = React.useState('') const filteredItems = items.filter( item => // design guidelines say to always show selected items in the list selected.some(selectedItem => selectedItem.text === item.text) || // then filter the rest item.text?.toLowerCase().startsWith(filter.toLowerCase()), ) // design guidelines say to sort selected items first const selectedItemsSortedFirst = filteredItems.sort((a, b) => { const aIsSelected = selected.some(selectedItem => selectedItem.text === a.text) const bIsSelected = selected.some(selectedItem => selectedItem.text === b.text) if (aIsSelected && !bIsSelected) return -1 if (!aIsSelected && bIsSelected) return 1 return 0 }) const [open, setOpen] = React.useState(false) return ( <FormControl> <FormControl.Label>Choices</FormControl.Label> <Button trailingAction={TriangleDownIcon} ref={buttonRef} onClick={() => setOpen(!open)}> {selected.map(selectedItem => selectedItem.text).join(', ') || 'Pick choices'} </Button> <SelectPanel anchorRef={buttonRef} renderAnchor={null} open={open} onOpenChange={setOpen} items={selectedItemsSortedFirst} selected={selected} onSelectedChange={setSelected} onFilterChange={setFilter} /> </FormControl> ) }