The SelectMenu
shares most of its behaviour, appearance and API with the
DropdownMenu
. Rather than accepting children you must provide options
as an
array, an onChange
handler and a value
.
Alternatives Dropdown — For presenting a list of discrete actions to a user
Standard Single select The single select is used for setting enumerable values when filtering data. It
is not to be used in forms, where an actual input element is required;
prefer the SelectField
.
const options = [ 'Chocolate' , 'Vanilla' , 'Strawberry' ] . map ( ( label ) => ( {
label ,
value : label . toLowerCase ( ) ,
} ) ) ;
const [ selected , setSelected ] = useState ( ) ;
const triggerLabel = 'Flavour' + ( selected ? ` : ${ selected . label } ` : '' ) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { setSelected }
options = { options }
value = { selected }
/>
) ;
Multi select The SelectMenu
is unopinionated in how many items are selected in the list, or
how you manage selected options. Pass an array to the value
prop to select
more items.
const options = [ 'Chocolate' , 'Vanilla' , 'Strawberry' ] . map ( ( label ) => ( {
label ,
value : label . toLowerCase ( ) ,
} ) ) ;
const [ selected , setSelected ] = useState ( [ ] ) ;
const handleChange = ( item ) => {
const some = ( opt ) => opt . value === item . value ;
const none = ( opt ) => opt . value !== item . value ;
if ( selected . some ( some ) ) {
setSelected ( ( old ) => old . filter ( none ) ) ;
} else {
setSelected ( ( old ) => old . concat ( item ) ) ;
}
} ;
const selectedLabel = selected . map ( ( opt ) => opt . label ) . join ( ', ' ) ;
const triggerLabel = 'Flavour' + ( selected . length ? ` : ${ selectedLabel } ` : '' ) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { handleChange }
options = { options }
value = { selected }
/>
) ;
Disabled option You may disable options where appropriate.
const options = [ 'Chocolate' , 'Vanilla' , 'Strawberry' ] . map ( ( label , index ) => ( {
label ,
value : label . toLowerCase ( ) ,
disabled : index === 2 ,
} ) ) ;
const [ selected , setSelected ] = useState ( ) ;
const triggerLabel = 'Flavour' + ( selected ? ` : ${ selected . label } ` : '' ) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { setSelected }
options = { options }
value = { selected }
/>
) ;
Filterable Allow the users to search for items within the menu. We recommend only
introducing filter behaviour when the expected option count is greater than 6.
The FilterMenu
shares most of the SelectMenu
API, with a few additions to
manage filtering -- checkout the API tab to learn more.
const fruits = [
'Apple' ,
'Banana' ,
'Cherry' ,
'Lemon' ,
'Mango' ,
'Nectarine' ,
'Orange' ,
'Peach' ,
'Raspberry' ,
'Strawberry' ,
'Watermelon' ,
] ;
const labelToOption = ( label ) => ( { label , value : label } ) ;
const compare = ( a , b ) => a . toLowerCase ( ) . includes ( b ?. toLowerCase ( ) ) ;
const [ selected , setSelected ] = useState ( ) ;
const [ options , setOptions ] = useState ( fruits . map ( labelToOption ) ) ;
const triggerLabel = 'Flavour' + ( selected ? ` : ${ selected . label } ` : '' ) ;
const onInputChange = ( input ) => {
setOptions (
fruits . filter ( ( label ) => compare ( label , input ) ) . map ( labelToOption )
) ;
} ;
return (
< FilterMenu
onChange = { setSelected }
onInputChange = { onInputChange }
options = { options }
trigger = { triggerLabel }
value = { selected }
/>
) ;
Async The FilterMenu
has no opinions about how you filter the options, whether it's
client or server side. If you fetch options onInputChange
, you must show the
user that it isLoading
. We recommend using debounce
(or similar) to
optimise performance.
const labelToOption = ( label ) => ( { label , value : label } ) ;
const compare = ( a , b ) => a . toLowerCase ( ) . includes ( b ?. toLowerCase ( ) ) ;
const [ selected , setSelected ] = useState ( ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ options , setOptions ] = useState ( [ ] ) ;
const triggerLabel = 'Fruit' + ( selected ? ` : ${ selected . label } ` : '' ) ;
let sleep = ( ms ) => new Promise ( ( r ) => setTimeout ( r , ms ) ) ;
async function onInputChange ( input ) {
if ( ! input ) {
setOptions ( [ ] ) ;
return ;
}
setLoading ( true ) ;
await sleep ( Math . random ( ) * 1000 ) ;
let res = [
'Apple' ,
'Banana' ,
'Cherry' ,
'Lemon' ,
'Mango' ,
'Nectarine' ,
'Orange' ,
'Peach' ,
'Raspberry' ,
'Strawberry' ,
'Watermelon' ,
] . filter ( ( label ) => compare ( label , input ) ) ;
setOptions ( res . map ( labelToOption ) ) ;
setLoading ( false ) ;
}
return (
< FilterMenu
placeholder = " Type to search "
isLoading = { loading }
onChange = { setSelected }
onInputChange = { onInputChange }
options = { options }
trigger = { triggerLabel }
value = { selected }
/>
) ;
Receipes Mapping Provide alternative properties in your options using the itemToDisabled
,
itemToValue
, and itemToLabel
utilities. Map complex objects to the required
data select-menu requires while using other properties in the itemRenderer
.
const [ selected , setSelected ] = useState ( ) ;
const [ loading , setLoading ] = useState ( false ) ;
const [ options , setOptions ] = useState ( [ ] ) ;
React . useEffect ( ( ) => {
const doFetch = async ( ) => {
let res = await fetch ( 'https://hp-api.herokuapp.com/api/characters' ) ;
let characters = await res . json ( ) ;
setOptions ( characters . filter ( ( c ) => c . house && c . dateOfBirth ) ) ;
} ;
doFetch ( ) ;
} , [ ] ) ;
const triggerLabel = 'Character' + ( selected ? ` : ${ selected . name } ` : '' ) ;
const toDisabled = ( i ) => i . name === 'Lord Voldemort' ;
return (
< SelectMenu
itemToDisabled = { toDisabled }
itemToLabel = { ( i ) => i . name }
itemToValue = { ( i ) => i . dateOfBirth }
onChange = { setSelected }
options = { options }
trigger = { triggerLabel }
value = { selected }
itemRenderer = { ( i ) => (
< div style = { { opacity : toDisabled ( i ) ? 0.5 : 1 } } >
< Text size = " small " weight = " medium " >
{ i . name }
</ Text >
< Text size = " xsmall " color = " muted " >
{ i . house } { i . hogwartsStaff ? '(teacher)' : null }
</ Text >
</ div >
) }
/>
) ;
Item renderer You can provide an alternate item renderer.
const statuses = [
'Neutral' ,
'Informative' ,
'Accent' ,
'Critical' ,
'Positive' ,
] . map ( ( label ) => ( { label , value : label . toLowerCase ( ) } ) ) ;
const users = [
'Tim Wilson' ,
'Alex Lee' ,
'Jason Smith' ,
'Mia Walker' ,
'Richie Chung' ,
] ;
const identity = ( i ) => i ;
const [ status , setStatus ] = useState ( ) ;
const [ user , setUser ] = useState ( ) ;
const statusLabel = 'Status' + ( status ? ` : ${ status . label } ` : '' ) ;
const userLabel = 'User' + ( user ? ` : ${ user } ` : '' ) ;
return (
< Inline gap = " medium " >
< SelectMenu
itemRenderer = { ( item ) => < Badge tone = { item . value } label = { item . label } /> }
trigger = { statusLabel }
onChange = { setStatus }
options = { statuses }
value = { status }
/>
< SelectMenu
itemRenderer = { ( item ) => (
< Inline alignY = " center " gap = " medium " >
< UserAvatar size = " xsmall " name = { item } />
< span > { item } </ span >
</ Inline >
) }
trigger = { userLabel }
onChange = { setUser }
options = { users }
itemToLabel = { identity }
itemToValue = { identity }
value = { user }
/>
</ Inline >
) ;
Clearable value Provide onClear
in addition to the onChange
handler to let the user clear
all the values with one click.
const flavours = [ 'Chocolate' , 'Vanilla' , 'Strawberry' , 'Caramel' , 'Mint' ] ;
const [ selected , setSelected ] = useState ( [ ] ) ;
const handleChange = ( flavour ) => {
if ( selected . includes ( flavour ) ) {
setSelected ( ( old ) => old . filter ( ( opt ) => opt !== flavour ) ) ;
} else {
setSelected ( ( old ) => old . concat ( flavour ) ) ;
}
} ;
const handleClear = ( ) => {
setSelected ( [ ] ) ;
} ;
const triggerLabel =
'Flavour' + ( selected . length ? ` : ${ selected . join ( ', ' ) } ` : '' ) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { handleChange }
onClear = { handleClear }
options = { flavours }
itemToLabel = { ( i ) => i }
itemToValue = { ( i ) => i }
value = { selected }
/>
) ;
Labelling There's a few strategies for displaying the value within the button.
Verbose The most user friendly approach is to display as much information as possible to
the user within your space constraints. In the example below we show up to 2
values, then when there's more than two we show the first selected value and the
remaining count.
const labels = [ 'Chocolate' , 'Vanilla' , 'Strawberry' , 'Caramel' , 'Mint' ] ;
const [ selected , setSelected ] = useState ( [ ] ) ;
const handleChange = ( item ) => {
const some = ( opt ) => opt . value === item . value ;
const none = ( opt ) => opt . value !== item . value ;
if ( selected . some ( some ) ) {
setSelected ( ( old ) => old . filter ( none ) ) ;
} else {
setSelected ( ( old ) => old . concat ( item ) ) ;
}
} ;
const selectedLabels = selected . map ( ( i ) => i . label ) ;
const selectedCount = selectedLabels . length ;
const triggerSuffix =
selectedCount > 2
? ` ${ selectedLabels [ 0 ] } + ${ selectedCount - 1 } more `
: selectedLabels . join ( ', ' ) ;
const triggerLabel = 'Flavour' + ( triggerSuffix ? ': ' : '' ) + triggerSuffix ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { handleChange }
options = { labels . map ( ( label , value ) => ( { label , value } ) ) }
value = { selected }
/>
) ;
Highlight You may want to highlight the selected value through font-weight and/or colour.
const options = [ 'Chocolate' , 'Vanilla' , 'Strawberry' , 'Caramel' , 'Mint' ] ;
const [ selected , setSelected ] = useState ( options [ 2 ] ) ;
const triggerLabel = (
< >
< Text leading = " tighter " size = " small " color = " muted " >
Flavour :
</ Text >
< Text leading = " tighter " size = " small " weight = " medium " color = " active " >
{ selected }
</ Text >
</ >
) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { setSelected }
options = { options }
itemToLabel = { ( i ) => i }
itemToValue = { ( i ) => i }
value = { selected }
/>
) ;
Indicator When horizontal space is at a premium you may want to display the count in
a notification badge.
const flavours = [ 'Chocolate' , 'Vanilla' , 'Strawberry' , 'Caramel' , 'Mint' ] ;
const [ selected , setSelected ] = useState ( flavours . slice ( 0 , 1 ) ) ;
const identity = ( i ) => i ;
const negate = ( predicate ) => ( ... args ) => ! predicate ( ... args ) ;
const handleChange = ( compare ) => {
const some = ( item ) => item === compare ;
const none = negate ( some ) ;
if ( selected . some ( some ) ) {
setSelected ( ( old ) => old . filter ( none ) ) ;
} else {
setSelected ( ( old ) => old . concat ( compare ) ) ;
}
} ;
const badge = < NotificationBadge tone = " informative " value = { selected . length } /> ;
const triggerLabel = (
< Inline gap = " xsmall " >
< Text > Flavour </ Text >
{ selected . length ? badge : null }
</ Inline >
) ;
return (
< SelectMenu
trigger = { triggerLabel }
onChange = { handleChange }
options = { flavours }
itemToLabel = { identity }
itemToValue = { identity }
value = { selected }
/>
) ;