useSelect
The problem
You have a custom select dropdown in your application and you want it to perform
exactly the same as the native HTML select in terms of accessibility and
functionality. For consistency reasons, you want it to follow the ARIA design
pattern for a select. You also want this solution to be simple to
use and flexible so you can tailor it to your needs.
This solution
useSelect is a React hook that manages all the stateful logic needed to make
the select functional and accessible. It returns a set of props that are meant
to be called, and their results destructured on the dropdown's elements: its
label, toggle button, list and list items. These are similar to the props
provided by vanilla Downshift to the children render prop.
These props are called getter props, and their return values are destructured as
a set of ARIA attributes and event listeners. Together with the action props and
state props, they create all the stateful logic needed for the dropdown to
implement the corresponding ARIA pattern. Every functionality needed should be
provided out-of-the-box: menu toggle, item selection and up/down movement
between them, screen reader support, highlight by character keys etc.
Breaking Changes in v8
useSelect has been affected by breaking changes in v8, so check out the
migration page.
Breaking Changes in v7
Since version 7, useSelect to supports the ARIA 1.2 pattern for the combobox,
which contains some changes from the ARIA 1.1 pattern. This brings changes in
the API and the behaviour of useSelect, detailed in the migration
page.
Props used in examples
In the examples below, we use the useSelect hook and destructure the getter
props and state variables it returns. These are used in the following way:
Returned prop | Element | Comments |
---|
getLabelProps | <label> | Call and destructure its returned object on the label element. |
getToggleButtonProps | <div> | Call and destructure its returned object on the toggle element. |
getMenuProps | <ul> | Call and destructure its returned object on the menu element. |
getItemProps | <li> | Call with index or item and destructure its returned object on each menu item element. |
isOpen | | State value with the open state of the menu. Used below for conditionally showing the items. |
highlightedIndex | | State value with the index of thehighlighted menu item. Used below for styling. |
selectedItem | | State value with the item that is selected. Used below for styling. |
For a complete documentation on all the returned props, hook props and more
information check out the Github Page.
Basic Usage
A custom select element can be created with HTML elements such as:
label, ul, li and button. Using other HTML elements to create a
custom select is useful for the custom styling of the widget, since the
select is notoriously difficult to style.
CodeSandbox for basic usage example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function Select() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
})
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{selectedItem ? selectedItem.title : 'Best book ever'}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={item.id}
{...getItemProps({item, index})}
>
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</li>
))}
</ul>
</div>
)
}
return <Select />
}
React Native
The hook can be used with React Native as well. The HTML elements and styles
are replaced with React Native equivalents, but the useSelect usage is exactly
the same as on React web.
MaterialUI
A custom select element can be created using UI Library components as well.
Many libraries provide basic elements such as buttons, texts/labels, and lists,
which can be styled according to each library guidelines. useSelect provides
the additional stateful logic to transform this selection of basic components
into a fully working dropdown element.
As useSelect needs to perform some focus() and scroll() logic on the
DOM elements, it requires refs to the React components used. This example
illustrates how to use useSelect with MaterialUI, and shows how to correctly
pass refs to the hook.
Since MaterialUI components already accept a ref prop that will be filled
with the resulting DOM element, we don't need to do anything specific other than
just spreading the getter props.
Another point worth mentioning is that in this case items are objects and not
strings. As a result, the itemToString prop is passed to useSelect. It will
return the string equivalent of the item which will be used for
selection/highlight by character keys and for the aria-live a11y
selectionmessage that will occur on every item selection: $ItemString has been
selected. item.title is chosen to be the string equivalent of each item
object.
CodeSandbox for MaterialUI usage example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function Select() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
})
return (
<Box>
<Box className="w-72 flex flex-col gap-1">
<FormLabel className="w-fit" {...getLabelProps()}>
Choose your favorite book:
</FormLabel>
<Button
component="div"
variant="contained"
color="secondary"
className="flex !justify-between"
{...getToggleButtonProps()}
>
{selectedItem ? itemToString(selectedItem) : 'Best book ever'}
{isOpen ? <ExpandLessIcon /> : <ExpandMoreIcon />}
</Button>
</Box>
<List
className={cx(
!isOpen && 'hidden',
'!absolute bg-white w-72 shadow-md max-h-80 overflow-scroll',
)}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => {
return (
<ListItem
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm',
)}
key={item.id}
{...getItemProps({
item,
index,
})}
>
<ListItemText primary={item.title} secondary={item.author} />
</ListItem>
)
})}
</List>
</Box>
)
}
return <Select />
}
Controlling state
Controlling state is possible by receiving the state changes handled by
Downshift via onChange props (onHighlightedIndexChange, onSelectedItemChange
etc.), changing data based on your requirements, and passing the data back to
Downshift via props, for instance highlightedIndex or selectedItem.
The example below shows how to control selectedItem. Both select elements
share the same selectedItem reference, and changing it in one of the dropdowns
will update the value in the other one as well.
CodeSandbox for controlling state example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function Select() {
const [selectedItem, setSelectedItem] = React.useState(null)
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
selectedItem,
onSelectedItemChange: ({selectedItem: newSelectedItem}) =>
setSelectedItem(newSelectedItem),
})
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{selectedItem ? selectedItem.title : 'Best book ever'}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={item.id}
{...getItemProps({item, index})}
>
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</li>
))}
</ul>
<p className="font-semibold">
{selectedItem
? `You have selected ${selectedItem.title} by ${selectedItem.author}.`
: 'Select a book!'}
</p>
</div>
)
}
return <Select />
}
State Reducer
For even more granular state change control, you can add your own reducer on top
of the default one. When the stateReducer is called, it will receive the
previous state and the actionAndChanges object as parameters.
actionAndChanges contains the change type, which explains why the state is
being changed. It also contains the changes proposed by useSelect that
should occur as a consequence of that change type. You are supposed to return
the new state according to your needs.
In the example below, we are implementing a Windows-specific behavior for the
select. While menu is closed, using ArrowUp and ArrowDown should keep the
menu closed and change selectedItem incrementally or decrementally. In the
stateReducer we capture the ToggleButtonKeyDownArrowDown and
ToggleButtonKeyDownArrowUp events, compute the next selectedItem based on
index, and return it without any other changes.
In all other state change types, we return useSelect default changes.
CodeSandbox for state reducer example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
if (state.isOpen) {
return changes
}
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
const nextItemIndex = books.indexOf(state.selectedItem)
if (nextItemIndex === books.length - 1) {
return {selectedItem: books[0]}
}
return {selectedItem: books[nextItemIndex + 1]}
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
const previousItemIndex = books.indexOf(state.selectedItem)
if (previousItemIndex === 0) {
return {selectedItem: books[books.length - 1]}
}
return {selectedItem: books[previousItemIndex - 1]}
default:
return changes
}
}
function Select() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
stateReducer,
})
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{selectedItem ? selectedItem.title : 'Best book ever'}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={item.id}
{...getItemProps({item, index})}
>
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</li>
))}
</ul>
</div>
)
}
return <Select />
}
Custom window
When using useSelect in an iframe or in any other scenario that uses a
window object different than the default browser window, it is required to
provide that window object to the hook as well. Internally, we rely on the
window for DOM related logic and working with the wrong object will make the
hook behave unexpectedly. For example, when using react-frame-component to
produce an iframe container, we should pass its window object to the hook
like shown below.
CodeSandbox for custom window example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function Select() {
const {window} = useFrame()
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
environment: window,
})
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{selectedItem ? selectedItem.title : 'Best book ever'}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={item.id}
{...getItemProps({item, index})}
>
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</li>
))}
</ul>
</div>
)
}
return (
<Frame className="h-60 w-full">
<link href="/styles.css" rel="stylesheet" />
<Select />
</Frame>
)
}
Basic Multiple selection
The useSelect hook can be used to create a widget that supports multiple
selection. In the example below, we mark each selected item with a checked
checkbox inside the menu list. Every other aspect remains the same as with the
single selection dropdown. For a more interactive example of multiple selection,
you can use our useMultipleSelection hook together with useSelect, as shown
in the multiple selection section.
In the example below, we control the selectedItem to always be null and keep
our selected items in a state variable, selectedItems. We use
onSelectedItemChange prop to retrieve the selectedItem from useSelect,
which is added to / removed from the selectedItems array. We also use
stateReducer to keep the menu open on selection by Enter key or by click, and
also to keep the highlightedIndex to be the most recent selected item.
In order to visually illustrate the selection, we render a checkbox before each
of them and check only the ones that are selected.
CodeSandbox for basic multiple selection
example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true,
highlightedIndex: state.highlightedIndex,
}
default:
return changes
}
}
function Select() {
const [selectedItems, setSelectedItems] = useState([])
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
stateReducer,
selectedItem: null,
onSelectedItemChange: ({selectedItem}) => {
if (!selectedItem) {
return
}
const index = selectedItems.indexOf(selectedItem)
if (index > 0) {
setSelectedItems([
...selectedItems.slice(0, index),
...selectedItems.slice(index + 1),
])
} else if (index === 0) {
setSelectedItems([...selectedItems.slice(1)])
} else {
setSelectedItems([...selectedItems, selectedItem])
}
},
})
const buttonText = selectedItems.length
? `${selectedItems.length} books selected.`
: 'Best books ever.'
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{buttonText}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItems.includes(item) && 'font-bold',
'py-2 px-3 shadow-sm flex gap-3 items-center',
)}
key={item.id}
{...getItemProps({
item,
index,
'aria-selected': selectedItems.includes(item),
})}
>
<input
type="checkbox"
className="h-5 w-5"
checked={selectedItems.includes(item)}
value={item}
onChange={() => null}
/>
<div className="flex flex-col">
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</div>
</li>
))}
</ul>
</div>
)
}
return <Select />
}
Using action props
Action props are functions returned by useSelect along with the state props
and getter props. They are handy when you need to execute select state changes
from event handlers, state change handlers or any other external location. In
the example below we open the menu when the toggle button is hovered, and clear
the selection by clicking on the custom selection clearing button. We use the
openMenu and selectItem action props in order to achieve these custom
behaviors.
CodeSandbox for action props example.
function SelectExample() {
const books = [
{id: 'book-1', author: 'Harper Lee', title: 'To Kill a Mockingbird'},
{id: 'book-2', author: 'Lev Tolstoy', title: 'War and Peace'},
{id: 'book-3', author: 'Fyodor Dostoyevsy', title: 'The Idiot'},
{id: 'book-4', author: 'Oscar Wilde', title: 'A Picture of Dorian Gray'},
{id: 'book-5', author: 'George Orwell', title: '1984'},
{id: 'book-6', author: 'Jane Austen', title: 'Pride and Prejudice'},
{id: 'book-7', author: 'Marcus Aurelius', title: 'Meditations'},
{
id: 'book-8',
author: 'Fyodor Dostoevsky',
title: 'The Brothers Karamazov',
},
{id: 'book-9', author: 'Lev Tolstoy', title: 'Anna Karenina'},
{id: 'book-10', author: 'Fyodor Dostoevsky', title: 'Crime and Punishment'},
]
function itemToString(item) {
return item ? item.title : ''
}
function Select() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
openMenu,
closeMenu,
selectItem,
} = useSelect({
items: books,
itemToString,
})
return (
<div onMouseLeave={closeMenu}>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div className="flex justify-between">
<div
className="p-2 bg-white flex justify-between grow cursor-pointer"
{...getToggleButtonProps({
onMouseEnter() {
openMenu()
},
})}
>
<span>
{selectedItem ? selectedItem.title : 'Best book ever'}
</span>
<span className="px-2">
{isOpen ? <>↑</> : <>↓</>}
</span>
</div>
<button
aria-label="clear selection"
className="p-2 bg-white"
type="button"
onClick={() => {
selectItem(null)
}}
>
×
</button>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen &&
books.map((item, index) => (
<li
className={cx(
highlightedIndex === index && 'bg-blue-300',
selectedItem === item && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={item.id}
{...getItemProps({item, index})}
>
<span>{item.title}</span>
<span className="text-sm text-gray-700">{item.author}</span>
</li>
))}
</ul>
</div>
)
}
return <Select />
}
Virtualizing items with react-virtual
When the number of items in the dropdown is too big, you may want to consider
using a virtualization technique to avoid loss in performance due to unnecessary
elements rendered in the DOM. react-virtual is a great
library to provide items virtualization and it's the one we will show in the
example below. There are other libraries as well, such as
react-virtualized and
react-virtual.
Since react-virtual has its own scrolling library, we will use it instead of
the default one from Downshift. Apart from this it's business as usual in both
the case of using useSelect and useVirtual, about which you can learn in the
react-virtual github link.
CodeSandbox for virtualized list example.
function SelectExample() {
const books = []
for (let index = 1; index <= 1000; index++) {
books.push({author: `Author ${index}`, title: `Book Number ${index}`})
}
function itemToString(item) {
return item ? item.title : ''
}
function estimateSize() {
return 60
}
function Select() {
const listRef = React.useRef()
const rowVirtualizer = useVirtual({
size: books.length,
parentRef: listRef,
estimateSize,
overscan: 2,
})
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({
items: books,
itemToString,
scrollIntoView() {},
onHighlightedIndexChange: ({highlightedIndex, type}) => {
if (type !== useSelect.stateChangeTypes.MenuMouseLeave) {
rowVirtualizer.scrollToIndex(highlightedIndex)
}
},
})
return (
<div>
<div className="w-72 flex flex-col gap-1">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div
className="p-2 bg-white flex justify-between cursor-pointer"
{...getToggleButtonProps()}
>
<span>{selectedItem ? selectedItem.title : 'Best book ever'}</span>
<span className="px-2">{isOpen ? <>↑</> : <>↓</>}</span>
</div>
</div>
<ul
className={`absolute w-72 bg-white mt-1 shadow-md max-h-80 overflow-scroll p-0 z-10 ${
!isOpen && 'hidden'
}`}
{...getMenuProps()}
>
{isOpen && (
<>
<li key="total-size" style={{height: rowVirtualizer.totalSize}} />
{rowVirtualizer.virtualItems.map(virtualRow => (
<li
className={cx(
highlightedIndex === virtualRow.index && 'bg-blue-300',
selectedItem === books[virtualRow.index] && 'font-bold',
'py-2 px-3 shadow-sm flex flex-col',
)}
key={books[virtualRow.index].id}
{...getItemProps({
index: virtualRow.index,
item: books[virtualRow.index],
})}
style={{
position: 'absolute',
top: 0,
left: 0,
width: '100%',
height: virtualRow.size,
transform: `translateY(${virtualRow.start}px)`,
}}
>
<span>{books[virtualRow.index].title}</span>
<span className="text-sm text-gray-700">
{books[virtualRow.index].author}
</span>
</li>
))}
</>
)}
</ul>
</div>
)
}
return <Select />
}
Other usage examples
To see more cool stuff you can build with useSelect, explore the examples
repository.