MultiSelect
MultiSelect allows users to choose one or more options from a list of items in a Menu.
Anatomy

- Label: The title of the input, positioned with none, top, or side alignment.
- Input: Similar to the single Select input.
- Pills: Selected items represented by removable Pills components.
- Search: Functionality to search through data within the Menu.
- Menu: Contains the list of selectable items.
Usage Guidance
- MultiSelect inputs feature a caret icon on the right side of the field.
- Clicking or tapping anywhere in the MultiSelect opens the Menu.
- A checkmark icon indicates selected values in the list.
- Each Menu option should be distinct; combine similar options if necessary.
- Sort Menu options logically: alphabetically, chronologically, or by importance.
- Display placeholder text (e.g., “Choose Items”) in the MultiSelect field.
- When focused, the placeholder becomes “Search,” and typed words replace it. An “x” icon appears to clear the search term.
- Clicking outside the input or Menu returns the MultiSelect to its default state.
- After making selections and dismissing the Menu, the input displays selected items as removable Pills in the same container.
When to use
- Use MultiSelect for selecting multiple items from a list of more than 7 predefined options.
- Ideal for lists between 7 to 100 items to prevent overwhelming users.
When to consider something else
- For yes/no options, use a Switch.
- For 2-7 predefined options, consider Radio Group for a single selection or Checkbox Group for multiple selections.
- Use a Prompt for large or unknown list sizes. Prompts offer search and folder navigation, supporting single or multiple selection.
Examples
Basic Example
MultiSelect supports a
dynamic API where you
pass an array of items via the items prop and provide a render function to display the items. The
items may be provided as an
array of strings or an
array of objects.
MultiSelect should be used in tandem with Form Field where the
MultiSelect wraps the FormField element and the FormField element wraps the children of
MultiSelect to meet accessibility standards. This ensures the label text from FormField is
attached to the MultiSelect.Input and read out as a group for voiceover.
<MultiSelect items={options}>
<FormField label="Your Label">
<MultiSelect.Input onChange={e => handleChange(e)} id="contact-multi-select" />
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => <MultiSelect.Item>{item.id}</MultiSelect.Item>}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>Disabled Example
Disabling MultiSelect involves passing the disabled prop to the MultiSelect.Input component.
import React from 'react';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {MultiSelect} from '@workday/canvas-kit-preview-react/multi-select';
const items = ['Cheese', 'Olives', 'Onions', 'Pepperoni', 'Peppers'];
export default () => {
return (
<>
<MultiSelect items={items} initialSelectedIds={['Olives', 'Onions', 'Pepperoni']}>
<FormField orientation="horizontalStart">
<FormField.Label>Toppings</FormField.Label>
<FormField.Input
as={MultiSelect.Input}
placeholder="Select Multiple"
removeLabel="Remove"
disabled
/>
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => (
<MultiSelect.Item data-id={item}>
<MultiSelect.Item.Text>{item}</MultiSelect.Item.Text>
</MultiSelect.Item>
)}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
</>
);
};
Error States
The MultiSelect.Input and MultiSelect.SearchInput support the ErrorType from the Common
package. The error styling is identical to the TextInput error styling. The error prop is
typically passed from the FormField component.
Select at least one topping.
import React from 'react';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {MultiSelect, useMultiSelectModel} from '@workday/canvas-kit-preview-react/multi-select';
const items = ['Cheese', 'Olives', 'Onions', 'Pepperoni', 'Peppers'];
export default () => {
const model = useMultiSelectModel({
items,
initialSelectedIds: [],
});
return (
<>
<MultiSelect model={model}>
<FormField
orientation="horizontalStart"
error={
model.state.selectedIds.length < 1
? 'error'
: model.state.selectedIds.length > 3
? 'caution'
: undefined
}
>
<FormField.Label>Toppings</FormField.Label>
<FormField.Field>
<FormField.Input
as={MultiSelect.Input}
placeholder="Select Multiple"
removeLabel="Remove"
/>
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => (
<MultiSelect.Item data-id={item}>
<MultiSelect.Item.Text>{item}</MultiSelect.Item.Text>
</MultiSelect.Item>
)}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
<FormField.Hint>
{model.state.selectedIds.length < 1
? 'Select at least one topping.'
: model.state.selectedIds.length > 3
? 'More than 3 toppings cost extra.'
: undefined}
</FormField.Hint>
</FormField.Field>
</FormField>
</MultiSelect>
</>
);
};
Complex
When registering items in an array of objects, it’s common to have the text that is displayed to the
user be different than an id. In this example, serverId and label properties need to be remapped
to id and text hence the usage of getId and getTextValue. If your object has the properties
text and id, there would be no need for this.
import React from 'react';
import {CanvasProvider} from '@workday/canvas-kit-react/common';
import {createStyles} from '@workday/canvas-kit-styling';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {system} from '@workday/canvas-tokens-web';
import {MultiSelect} from '@workday/canvas-kit-preview-react/multi-select';
const mainContentStyles = createStyles({
padding: system.space.x4,
});
const items = [
{id: '1', text: 'Cheese'},
{id: '2', text: 'Olives'},
{id: '3', text: 'Onions'},
{id: '4', text: 'Pepperoni'},
{id: '5', text: 'Peppers'},
];
export default () => {
const [value, setValue] = React.useState('');
const [label, setLabel] = React.useState('');
return (
<CanvasProvider>
<>
<form
onSubmit={e => {
console.log('form submitted');
e.preventDefault();
}}
>
<main className={mainContentStyles}>
<MultiSelect items={items} getId={i => i.id} getTextValue={i => i.text}>
<FormField orientation="horizontalStart">
<FormField.Label>Toppings</FormField.Label>
<FormField.Input
as={MultiSelect.Input}
placeholder="Select Multiple"
removeLabel="Remove"
name="toppings"
onChange={e => {
const value = e.currentTarget.value;
setValue(value);
setLabel(
value
.split(', ')
.map(item => items.find(i => i.id === item)?.text || 'Not Found')
.join(', ')
);
}}
value={value}
/>
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => (
<MultiSelect.Item data-id={item.id}>
<MultiSelect.Item.Text>{item.text}</MultiSelect.Item.Text>
</MultiSelect.Item>
)}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
</main>
</form>
<div>Selected IDs: {value}</div>
<div>Selected Labels: {label}</div>
</>
</CanvasProvider>
);
};
With Icons
Use MultiSelect.Item.Icon to render an icon for a MultiSelect.Item. The icon prop for
MultiSelect.Item.Icon accepts system icons from
@workday/canvas-system-icons-web.
Note:
data-idonMultiSelect.Itemmust match theidproperty in your array of objects. This ensures proper keyboard handling and type-ahead.
import React from 'react';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {
mediaPauseIcon,
mediaPlayIcon,
mediaTopicsIcon,
skipIcon,
previousIcon,
} from '@workday/canvas-system-icons-web';
import {MultiSelect} from '@workday/canvas-kit-preview-react/multi-select';
const items = [
{id: '1', text: 'Pause', icon: mediaPauseIcon},
{id: '2', text: 'Play', icon: mediaPlayIcon},
{id: '3', text: 'Skip', icon: skipIcon},
{id: '4', text: 'Previous', icon: previousIcon},
];
export default () => {
return (
<MultiSelect items={items}>
<FormField orientation="horizontalStart">
<FormField.Label>Controls</FormField.Label>
<FormField.Input
as={MultiSelect.Input}
placeholder="Select Multiple"
removeLabel="Remove"
/>
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => (
<MultiSelect.Item data-id={item.id}>
<MultiSelect.Item.Icon icon={item.icon} />
<MultiSelect.Item.Text>{item.text}</MultiSelect.Item.Text>
<MultiSelect.Item.Icon icon={mediaTopicsIcon} />
</MultiSelect.Item>
)}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
);
};
Controlled
The MultiSelect can be a controlled input component by passing the value and onChange to either
the <MultiSelect> component or the <MultiSelect.Input> component. Internally, the
MultiSelect.Input watches for changes on the value React prop as well as the value DOM
property and will update the model accordingly.
import React from 'react';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {PrimaryButton, SecondaryButton} from '@workday/canvas-kit-react/button';
import {Flex} from '@workday/canvas-kit-react/layout';
import {MultiSelect} from '@workday/canvas-kit-preview-react/multi-select';
const items = [
{id: '1', text: 'Cheese'},
{id: '2', text: 'Olives'},
{id: '3', text: 'Onions'},
{id: '4', text: 'Pepperoni'},
{id: '5', text: 'Peppers'},
];
export default () => {
const formRef = React.useRef<HTMLFormElement>(null);
const [value, setValue] = React.useState('1');
const [label, setLabel] = React.useState('Cheese');
function handleOnChange(event: React.ChangeEvent<HTMLInputElement>) {
const value = event.currentTarget.value;
setValue(value);
setLabel(
value
.split(', ')
.map(item => items.find(i => i.id === item)?.text || 'Not Found')
.join(', ')
);
}
return (
<>
<form
onSubmit={e => {
console.log('form submitted');
e.preventDefault();
}}
ref={formRef}
>
<Flex gap="s" flexDirection="column">
<MultiSelect items={items}>
<FormField orientation="horizontalStart">
<FormField.Label>Toppings</FormField.Label>
<FormField.Input
as={MultiSelect.Input}
placeholder="Select Multiple"
removeLabel="Remove"
name="toppings"
onChange={handleOnChange}
value={value}
/>
<MultiSelect.Popper>
<MultiSelect.Card>
<MultiSelect.List>
{item => (
<MultiSelect.Item data-id={item.id}>
<MultiSelect.Item.Text>{item.text}</MultiSelect.Item.Text>
</MultiSelect.Item>
)}
</MultiSelect.List>
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
<Flex gap="s">
<SecondaryButton
onClick={e => {
setValue('1, 2, 3');
}}
>
Set to "Cheese, Olives, Onions" via React `value`
</SecondaryButton>
<SecondaryButton
onClick={e => {
const input = formRef.current.querySelector('[name=toppings]') as HTMLInputElement;
input.value = '1, 2';
}}
>
Set to "Cheese, Olives" via DOM `value`
</SecondaryButton>
</Flex>
<div>
<PrimaryButton type="submit">Submit</PrimaryButton>
</div>
<div>Selected ID: {value}</div>
<div>Selected Label: {label}</div>
</Flex>
</form>
</>
);
};
Searching
A MultiSelect input can be used as a filter for results. Most likely this also means there are many
items that may not be all be loaded from the server at once. The useComboboxLoader can be used to
dynamically load items as the user navigates the available options.
Note: The behavior of search is experimental. The example should continue to work without modification, but how the searchable input is presented to the user may change with user testing. Don’t rely too much on the exact behavior of the search input. For example, the search input may be cleared when the user blurs the field.
import React from 'react';
import {system} from '@workday/canvas-tokens-web';
import {createStyles} from '@workday/canvas-kit-styling';
import {LoadReturn} from '@workday/canvas-kit-react/collection';
import {CanvasProvider} from '@workday/canvas-kit-react/common';
import {useComboboxLoader} from '@workday/canvas-kit-react/combobox';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {MultiSelect, useMultiSelectModel} from '@workday/canvas-kit-preview-react/multi-select';
import {StyledMenuItem} from '@workday/canvas-kit-react/menu';
const mainContentStyles = createStyles({
padding: system.space.x4,
});
const colors = ['Red', 'Blue', 'Purple', 'Green', 'Pink'];
const fruits = ['Apple', 'Orange', 'Banana', 'Grape', 'Lemon', 'Lime'];
const options = Array(1000)
.fill('')
.map((_, index) => {
return {
id: `${index + 1}`,
text: `${colors[index % colors.length]} ${fruits[index % fruits.length]} ${index + 1}`,
};
});
export default () => {
const [value, setValue] = React.useState('');
const {model, loader} = useComboboxLoader(
{
// You can start with any number that makes sense.
total: 0,
// Pick whatever number makes sense for your API
pageSize: 20,
// A load function that will be called by the loader. You must return a promise that returns
// an object like `{items: [], total: 0}`. The `items` will be merged into the loader's cache
async load({pageNumber, pageSize, filter}) {
return new Promise<LoadReturn<(typeof options)[0]>>(resolve => {
// simulate a server response by resolving after a period of time
setTimeout(() => {
// simulate paging and filtering based on pre-computed items
const start = (pageNumber - 1) * pageSize;
const end = start + pageSize;
const filteredItems = options.filter(item => {
if (filter === '' || typeof filter !== 'string') {
return true;
}
return item.text.toLowerCase().includes(filter.toLowerCase());
});
const total = filteredItems.length;
const items = filteredItems.slice(start, end);
resolve({
items,
total,
});
}, 300);
});
},
onShow() {
// The `shouldLoad` cancels while the combobox menu is hidden, so let's load when it is
// visible
loader.load();
},
},
useMultiSelectModel
);
return (
<CanvasProvider>
<>
<form
onSubmit={e => {
console.log('form submitted');
e.preventDefault();
}}
>
<main className={mainContentStyles}>
<MultiSelect model={model}>
<FormField orientation="horizontalStart">
<FormField.Label>Fruits</FormField.Label>
<FormField.Input
as={MultiSelect.SearchInput}
placeholder="Search"
removeLabel="Remove"
name="toppings"
onChange={e => {
setValue(e.currentTarget.value);
}}
value={value}
/>
<MultiSelect.Popper>
<MultiSelect.Card>
{model.state.items.length === 0 && (
<StyledMenuItem as="span">No Results Found</StyledMenuItem>
)}
{model.state.items.length > 0 && (
<MultiSelect.List maxHeight={200}>
{item =>
item ? (
<MultiSelect.Item data-id={item.id}>
<MultiSelect.Item.Text>{item.text}</MultiSelect.Item.Text>
</MultiSelect.Item>
) : undefined
}
</MultiSelect.List>
)}
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
</main>
</form>
<div>Selected: {value}</div>
</>
</CanvasProvider>
);
};
Initial Selected Items
You can set initialSelectedIds to the value that you want initially selected.
import React, {useEffect} from 'react';
import {system} from '@workday/canvas-tokens-web';
import {createStyles} from '@workday/canvas-kit-styling';
import {LoadReturn} from '@workday/canvas-kit-react/collection';
import {CanvasProvider, useMountLayout} from '@workday/canvas-kit-react/common';
import {useComboboxLoader} from '@workday/canvas-kit-react/combobox';
import {FormField} from '@workday/canvas-kit-react/form-field';
import {MultiSelect, useMultiSelectModel} from '@workday/canvas-kit-preview-react/multi-select';
import {StyledMenuItem} from '@workday/canvas-kit-react/menu';
const mainContentStyles = createStyles({
padding: system.space.x4,
});
const colors = ['Red', 'Blue', 'Purple', 'Green', 'Pink'];
const fruits = ['Apple', 'Orange', 'Banana', 'Grape', 'Lemon', 'Lime'];
const options = Array(1000)
.fill('')
.map((_, index) => {
return {
id: `${index + 1}`,
text: `${colors[index % colors.length]} ${fruits[index % fruits.length]} ${index + 1}`,
};
});
export default () => {
const [value, setValue] = React.useState('');
const {model, loader} = useComboboxLoader(
{
// You can start with any number that makes sense.
total: 0,
initialSelectedIds: ['3', '5'],
// Pick whatever number makes sense for your API
pageSize: 500,
// A load function that will be called by the loader. You must return a promise that returns
// an object like `{items: [], total: 0}`. The `items` will be merged into the loader's cache
async load({pageNumber, pageSize, filter}) {
return new Promise<LoadReturn<(typeof options)[0]>>(resolve => {
// simulate a server response by resolving after a period of time
setTimeout(() => {
// simulate paging and filtering based on pre-computed items
const start = (pageNumber - 1) * pageSize;
const end = start + pageSize;
const filteredItems = options.filter(item => {
if (filter === '' || typeof filter !== 'string') {
return true;
}
return item.text.toLowerCase().includes(filter.toLowerCase());
});
const total = filteredItems.length;
const items = filteredItems.slice(start, end);
resolve({
items,
total,
});
}, 300);
});
},
onShow() {
// The `shouldLoad` cancels while the combobox menu is hidden, so let's load when it is
// visible
loader.load();
},
},
useMultiSelectModel
);
useEffect(() => {
loader.load();
}, [loader]);
return (
<CanvasProvider>
<>
<form
onSubmit={e => {
console.log('form submitted');
e.preventDefault();
}}
>
<main className={mainContentStyles}>
<MultiSelect model={model}>
<FormField orientation="horizontalStart">
<FormField.Label>Fruits</FormField.Label>
<FormField.Input
as={MultiSelect.SearchInput}
placeholder="Search"
removeLabel="Remove"
name="toppings"
onChange={e => {
setValue(e.currentTarget.value);
}}
value={value}
/>
<MultiSelect.Popper>
<MultiSelect.Card>
{model.state.items.length === 0 && (
<StyledMenuItem as="span">No Results Found</StyledMenuItem>
)}
{model.state.items.length > 0 && (
<MultiSelect.List maxHeight={200}>
{item =>
item ? (
<MultiSelect.Item data-id={item.id}>
<MultiSelect.Item.Text>{item.text}</MultiSelect.Item.Text>
</MultiSelect.Item>
) : undefined
}
</MultiSelect.List>
)}
</MultiSelect.Card>
</MultiSelect.Popper>
</FormField>
</MultiSelect>
</main>
</form>
<div>Selected: {value}</div>
</>
</CanvasProvider>
);
};
Accessibility Guidelines
Keyboard Interaction
Each MultiSelect must have a focus indicator that is highly visible against the background and against the non-focused state. Refer to Accessible Colors for more information.
MultiSelect must support the following keyboard interactions:
Tab: focus the MultiSelect componentEnterorSpace: open the Menu and focus on the first optionEnterorSpace: selects and deselects the item currently in focusEsc: dismiss the MultiSelect Menu and focus the MultiSelect componentUp ArroworDown Arrow: focus the previous or next option respectivelyCharacter Key: focus options matching character keyHomeorFn + Up Arrow: focus first optionEndorFn + Down Arrow: focus last option
MultiSelect Keyboard Navigation when Search feature is enabled:
Space: when typing a search term, will add a spaceEnterorSpace: selects and deselects the item currently in focus- All other keyboard interaction remains the same
Screen Reader Interaction
MultiSelect must communicate the following to users:
- This component is a “combobox”
- The associated label
- The currently selected value
- The “collapsed” or “expanded” state
- When opened, there is a list of ‘X’ items
- When opened, the name of the active option
- When opened, the position “X of Y” for the active option
Content Guidelines
- The list of Menu items should be scannable, with concise labels written in title case. Don’t write sentences and omit articles (a, an, the) unless needed for clarity. For more detailed information on how to write Menu items, refer to the Menus section of the Content Style Guide.
- Placeholder text for a Select must begin with the verb “Select”. Refer to the guidelines on Placeholder Text in the Content Style Guide for more tips on how to write placeholder text.
Can't Find What You Need?
Check out our FAQ section which may help you find the information you're looking for. For further information, contact the #ask-canvas-design or #ask-canvas-kitchannels on Slack.