File: CHANGELOG.md
CHANGELOG
The changelog is automatically updated using
semantic-release. You
can see it on the releases page.
File: CODE_OF_CONDUCT.md
Contributor Covenant Code of Conduct
Table of Contents
Our Pledge
In the interest of fostering an open and welcoming environment, we as
contributors and maintainers pledge to making participation in our project and
our community a harassment-free experience for everyone, regardless of age, body
size, disability, ethnicity, gender identity and expression, level of
experience, nationality, personal appearance, race, religion, or sexual identity
and orientation.
Our Standards
Examples of behavior that contributes to creating a positive environment
include:
- Using welcoming and inclusive language
- Being respectful of differing viewpoints and experiences
- Gracefully accepting constructive criticism
- Focusing on what is best for the community
- Showing empathy towards other community members
Examples of unacceptable behavior by participants include:
- The use of sexualized language or imagery and unwelcome sexual attention or
advances - Trolling, insulting/derogatory comments, and personal or political attacks
- Public or private harassment
- Publishing others' private information, such as a physical or electronic
address, without explicit permission - Other conduct which could reasonably be considered inappropriate in a
professional setting
Our Responsibilities
Project maintainers are responsible for clarifying the standards of acceptable
behavior and are expected to take appropriate and fair corrective action in
response to any instances of unacceptable behavior.
Project maintainers have the right and responsibility to remove, edit, or reject
comments, commits, code, wiki edits, issues, and other contributions that are
not aligned to this Code of Conduct, or to ban temporarily or permanently any
contributor for other behaviors that they deem inappropriate, threatening,
offensive, or harmful.
Scope
This Code of Conduct applies both within project spaces and in public spaces
when an individual is representing the project or its community. Examples of
representing a project or community include using an official project e-mail
address, posting via an official social media account, or acting as an appointed
representative at an online or offline event. Representation of a project may be
further defined and clarified by project maintainers.
Enforcement
Instances of abusive, harassing, or otherwise unacceptable behavior may be
reported by contacting the project team at [email protected]. All
complaints will be reviewed and investigated and will result in a response that
is deemed necessary and appropriate to the circumstances. The project team is
obligated to maintain confidentiality with regard to the reporter of an
incident. Further details of specific enforcement policies may be posted
separately.
Project maintainers who do not follow or enforce the Code of Conduct in good
faith may face temporary or permanent repercussions as determined by other
members of the project's leadership.
Attribution
This Code of Conduct is adapted from the Contributor Covenant,
version 1.4, available at http://contributor-covenant.org/version/1/4
File: CONTRIBUTING.md
Contributing
Thanks for being willing to contribute!
Working on your first Pull Request? You can learn how from this free
series How to Contribute to an Open Source Project on GitHub
Project setup
- Fork and clone the repo
npm run setupto setup and validate your clone of the project- Create a branch for your PR
Tip: Keep your
masterbranch pointing at the original repository and make
pull requests from branches on your fork. To do this, run:textgit remote add upstream https://github.com/downshift-js/downshift.git git fetch upstream git branch --set-upstream-to=upstream/master masterThis will add the original repository as a "remote" called "upstream," Then
fetch the git information from that remote, then set your localmaster
branch to use the upstream master branch whenever you rungit pull. Then you
can make all of your pull request branches based on thismasterbranch.
Whenever you want to update your version ofmaster, do a regulargit pull.
Committing and Pushing changes
Please make sure to run the tests before you commit your changes. You can runnpm run test:update which will update any snapshots that need updating. Make
sure to include those changes (if they exist) in your commit. We also track the
bundle sizes in a .size-snapshot.json file, this will likely update when you
make changes to the codebase.
Tests
There are quite a few test scripts that run as part of a validate script in
this project:
- lint - ESLint stuff, pretty basic. Please fix any errors/warnings :)
- build-and-test - This ensures that the built version of
downshiftis what we
expect. These tests live inother/misc-tests/__tests__. - test:cover - This is primarily unit tests on the source code and accounts for
most of the coverage. We enforce 100% code coverage on this library. These
tests live insrc/__tests__ - test:ts - This runs
tscon the codebase to make sure the type script
definitions are correct for thetsxfiles in thetestdirectory. - test:ssr - This ensures that downshift works with server-side rendering (it
can run and render in an environment without the DOM). These tests live inother/ssr/__tests__ - test:e2e - This runs tests in an actual browser. It runs and tests the
docusaurus examples. These tests live ine2e/.
opt into git hooks
There are git hooks set up with this project that are automatically installed
when you install dependencies. They're really handy, but are turned off by
default (so as to not hinder new contributors). You can opt into these by
creating a file called .opt-in at the root of the project and putting this
inside:
pre-commitHelp needed
Please checkout the the open issues
Also, please watch the repo and respond to questions/bug reports/feature
requests! Thanks!
File: package.json
{
"name": "downshift",
"version": "0.0.0-semantically-released",
"description": "🏎 A set of primitives to build simple, flexible, WAI-ARIA compliant React autocomplete, combobox or select dropdown components.",
"main": "dist/downshift.cjs.cjs",
"react-native": "dist/downshift.native.cjs.cjs",
"module": "dist/downshift.esm.mjs",
"typings": "typings/index.d.ts",
"types": "typings/index.d.ts",
"sideEffects": false,
"browserslist": [],
"scripts": {
"build": "npm run build:web --silent && npm run build:native --silent && npm run build:nativeWeb --silent",
"build:web": "kcd-scripts build --bundle --p-react --no-clean --size-snapshot",
"build:native": "cross-env BUILD_REACT_NATIVE=true BUILD_FILENAME_SUFFIX=.native kcd-scripts build --bundle cjs --no-clean",
"build:nativeWeb": "cross-env BUILD_REACT_NATIVE_WEB=true BUILD_FILENAME_SUFFIX=.nativeweb kcd-scripts build --bundle cjs --no-clean",
"lint": "kcd-scripts lint",
"test": "kcd-scripts test",
"test:cover": "kcd-scripts test --coverage",
"test:ssr": "kcd-scripts test --config other/ssr/jest.config.js --no-watch",
"test:update": "npm run test:cover -s -- --updateSnapshot",
"test:ts": "tsc --noEmit -p ./tsconfig.json",
"test:flow": "flow",
"test:flow:coverage": "flow-coverage-report",
"test:build": "jest --config other/misc-tests/jest.config.js",
"// FIXME: test:build": "jest --projects other/misc-tests other/react-native",
"test:e2e:dev": "playwright test --ui",
"pretest:e2e": "npm run docs:build --silent",
"test:e2e": "playwright test",
"pw:run": "playwright test",
"pw:open": "playwright test --ui",
"build-and-test": "npm run build -s && npm run test:build -s",
"docs:build": "docusaurus build",
"docs:dev": "docusaurus start",
"docs:serve": "docusaurus serve --port 6006",
"docs:clear": "docusaurus clear",
"setup": "npm install && npm run validate",
"validate": "kcd-scripts validate lint,build-and-test,test:cover,test:ts,test:ssr,test:e2e"
},
"husky": {
"hooks": {
"pre-commit": "kcd-scripts pre-commit"
}
},
"lint-staged": {
".+(js|jsx|json|yml|yaml|css|less|scss|ts|tsx|md|gql|graphql|mdx|vue)": [
"kcd-scripts format",
"kcd-scripts lint",
"kcd-scripts test --findRelatedTests --passWithNoTests"
]
},
"files": [
"dist",
"typings",
"preact",
"flow-typed"
],
"exports": {
".": {
"import": "./dist/downshift.esm.mjs",
"require": "./dist/downshift.cjs.cjs",
"types": "./typings/index.d.ts",
"default": "./dist/downshift.esm.mjs"
},
"./react-native": {
"require": "./dist/downshift.native.cjs.cjs",
"types": "./typings/index.d.ts"
}
},
"keywords": [
"enhanced input",
"react",
"autocomplete",
"autosuggest",
"typeahead",
"dropdown",
"select",
"combobox",
"omnibox",
"accessibility",
"WAI-ARIA",
"multiselect",
"multiple selection"
],
"author": "Kent C. Dodds [email protected] (http://kentcdodds.com/)",
"license": "MIT",
"peerDependencies": {
"react": ">=16.12.0"
},
"dependencies": {
"@babel/runtime": "^7.28.6",
"compute-scroll-into-view": "^3.1.1",
"prop-types": "^15.8.1",
"react-is": "^18.2.0",
"tslib": "^2.8.1"
},
"devDependencies": {
"@babel/helpers": "^7.28.6",
"@docusaurus/babel": "3.10.1",
"@docusaurus/core": "3.10.1",
"@docusaurus/module-type-aliases": "3.10.1",
"@docusaurus/preset-classic": "3.10.1",
"@mdx-js/react": "^3.0.1",
"@playwright/test": "^1.60.0",
"@rollup/plugin-babel": "^6.1.0",
"@rollup/plugin-commonjs": "^29.0.0",
"@testing-library/dom": "^10.4.1",
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/preact": "^3.2.4",
"@testing-library/react": "^16.3.2",
"@testing-library/user-event": "^14.6.1",
"@types/jest": "^30.0.0",
"@types/prop-types": "^15.7.15",
"@types/react": "^18.2.0",
"@typescript-eslint/eslint-plugin": "^8.54.0",
"@typescript-eslint/parser": "^8.54.0",
"babel-plugin-macros": "^3.1.0",
"babel-plugin-no-side-effect-class-properties": "0.0.7",
"babel-preset-react-native": "^4.0.1",
"buble": "^0.20.0",
"cpy-cli": "^6.0.0",
"cross-env": "^10.1.0",
"eslint": "^8.57.0",
"eslint-plugin-react": "7.37.5",
"eslint-plugin-react-hooks": "^7.1.1",
"flow-bin": "^0.299.0",
"flow-coverage-report": "^0.8.0",
"get-pkg-repo": "5.0.0",
"kcd-scripts": "^17.0.0",
"node-polyfill-webpack-plugin": "^4.1.0",
"npm-run-all": "^4.1.5",
"preact": "^10.28.2",
"prism-react-renderer": "^2.4.1",
"react": "^18.3.1",
"react-compiler-runtime": "^1.0.0",
"react-dom": "^18.3.1",
"react-is": "^18.3.1",
"react-native": "^0.76.0",
"react-test-renderer": "^18.3.1",
"serve": "^14.2.5",
"start-server-and-test": "^2.1.3",
"typescript": "^5.9.3"
},
"overrides": {
"eslint-plugin-react-hooks": "^7.1.1"
},
"eslintConfig": {
"parserOptions": {
"ecmaVersion": 2023,
"project": [
"./tsconfig.json",
"./docusaurus/tsconfig.json",
"./test/tsconfig.json",
"./e2e/tsconfig.json"
],
"sourceType": "module"
},
"settings": {
"import/no-unresolved": [
2,
{
"ignore": [
"^@theme"
]
}
],
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx"
]
}
}
},
"extends": "./node_modules/kcd-scripts/eslint.js",
"rules": {
"react-hooks/refs": "error",
"react/jsx-indent": "off",
"react/prop-types": "off",
"max-lines-per-function": "off",
"jsx-a11y/label-has-for": "off",
"jsx-a11y/label-has-associated-control": "off",
"jsx-a11y/autocomplete-valid": "off",
"testing-library/prefer-user-event": "off",
"testing-library/no-node-access": "off",
"testing-library/no-container": "off",
"testing-library/render-result-naming-convention": "off"
},
"overrides": [
{
"files": [
"e2e/**/.ts"
],
"rules": {
"testing-library/prefer-screen-queries": "off",
"import/no-extraneous-dependencies": [
"error",
{
"devDependencies": true
}
]
}
}
]
},
"eslintIgnore": [
"node_modules",
"coverage",
"dist",
".docusaurus",
"build",
"test",
"typings",
"playwright-report",
"test-results"
],
"repository": {
"type": "git",
"url": "https://github.com/downshift-js/downshift.git"
},
"bugs": {
"url": "https://github.com/downshift-js/downshift/issues"
},
"homepage": "https://downshift-js.com",
"flow-coverage-report": {
"includeGlob": [
"test/**/*.js"
],
"threshold": 90,
"type": [
"text"
]
}
}
File: tsconfig.json
{
"compilerOptions": {
"allowJs": true,
"esModuleInterop": true,
"jsx": "react",
"moduleResolution": "bundler",
"strict": true,
"skipLibCheck": true,
"resolveJsonModule": true,
"noUncheckedIndexedAccess": true,
"module": "ESNext",
"typeRoots": ["./node_modules/@types", "./node_modules/@testing-library"],
"types": ["jest", "jest-dom", "node"],
"strictNullChecks": true,
"rootDir": "src",
"outDir": "dist",
"declaration": true,
"declarationDir": "dist",
"emitDeclarationOnly": true
},
"include": ["src//*.ts", "src//*.tsx"],
"exclude": ["dist"]
}
File: tsconfig.preact.json
{
"extends": "./tsconfig.json",
"compilerOptions": {
"outDir": "preact/dist"
}
}
File: docusaurus/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true,
"rootDir": ".."
},
"include": [".//.tsx", "../typings//*.d.ts"]
}
File: e2e/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["./**/*.ts", "../playwright.config.ts"],
"exclude": []
}
File: other/MAINTAINING.md
Maintaining
Table of Contents
This is documentation for maintainers of this project.
Code of Conduct
Please review, understand, and be an example of it. Violations of the code of
conduct are taken seriously, even (especially) for maintainers.
Issues
We want to support and build the community. We do that best by helping people
learn to solve their own problems. We have an issue template and hopefully most
folks follow it. If it's not clear what the issue is, invite them to create a
minimal reproduction of what they're trying to accomplish or the bug they think
they've found.
Once it's determined that a code change is necessary, point people to
makeapullrequest.com and invite them to make a
pull request. If they're the one who needs the feature, they're the one who can
build it. If they need some hand holding and you have time to lend a hand,
please do so. It's an investment into another human being, and an investment
into a potential maintainer.
Remember that this is open source, so the code is not yours, it's ours. If
someone needs a change in the codebase, you don't have to make it happen
yourself. Commit as much time to the project as you want/need to. Nobody can ask
any more of you than that.
Pull Requests
As a maintainer, you're fine to make your branches on the main repo or on your
own fork. Either way is fine.
When we receive a pull request, a travis build is kicked off automatically (see
the .travis.yml for what runs in the travis build). We avoid merging anything
that breaks the travis build.
Please review PRs and focus on the code rather than the individual. You never
know when this is someone's first ever PR and we want their experience to be as
positive as possible, so be uplifting and constructive.
When you merge the pull request, 99% of the time you should use the
Squash and merge
feature. This keeps our git history clean, but more importantly, this allows us
to make any necessary changes to the commit message so we release what we want
to release. See the next section on Releases for more about that.
Release
Our releases are automatic. They happen whenever code lands into master. A
travis build gets kicked off and if it's successful, a tool calledsemantic-release is
used to automatically publish a new release to npm as well as a changelog to
GitHub. It is only able to determine the version and whether a release is
necessary by the git commit messages. With this in mind, please brush up on
the commit message convention which drives our releases.
One important note about this: Please make sure that commit messages do NOT
contain the words "BREAKING CHANGE" in them unless we want to push a major
version. I've been burned by this more than once where someone will include
"BREAKING CHANGE: None" and it will end up releasing a new major version. Not
a huge deal honestly, but kind of annoying...
Thanks!
Thank you so much for helping to maintain this project!
File: other/manual-releases.md
manual-releases
This project has an automated release set up. So things are only released when
there are useful changes in the code that justify a release. But sometimes
things get messed up one way or another and we need to trigger the release
ourselves. When this happens, simply bump the number below and commit that with
the following commit message based on your needs:
Major
fix(release): manually release a major version
There was an issue with a major release, so this manual-releases.md
change is to release a new major version.
Reference: #<the number of a relevant pull request, issue, or commit>
BREAKING CHANGE: <mention any relevant breaking changes (this is what triggers the major version change so don't skip this!)>Minor
feat(release): manually release a minor version
There was an issue with a minor release, so this manual-releases.md
change is to release a new minor version.
Reference: #<the number of a relevant pull request, issue, or commit>Patch
fix(release): manually release a patch version
There was an issue with a patch release, so this manual-releases.md
change is to release a new patch version.
Reference: #<the number of a relevant pull request, issue, or commit>The number of times we've had to do a manual release is: 12
File: other/TYPESCRIPT_USAGE.md
Typescript Usage
The current bundled Typescript definitions are incomplete and based around the
needs of the developers who contributed them.
Pull requests to improve them are welcome and appreciated. If you've never
contributed to open source before, then you may find
this free video course
helpful.
File: other/USERS.md
Users
If you or your company uses this project, add your name to this list! Eventually
we may have a website to showcase these (wanna build it!?)
No users have been added yet!
File: src/hooks/README.md
Downshift Hooks
A set of hooks to build simple, flexible, WAI-ARIA compliant React dropdown
components. Developed as a follow up on this issue about using
hooks in our API.
Migration to v7
useSelect and useCombobox received some changes related to their API and how
they works in version 7, as a conequence of adapting both hooks to the ARIA 1.2
combobox patterns. If you were using useSelect and/or useCombobox previous
to 7.0.0, check the migration guide and update if necessary.
Hooks
Check out one of the hooks below to use in your application and create fully
accessible widgets without any constraint about the UI library used.
useSelect
For a custom select dropdown check out useSelect.
useCombobox
For a combobox/autocomplete input check out useCombobox.
useTagGroup
For a tag group that could also be used to build a multiple selection select
or a combobox with tags, check out useTagGroup.
Downshift Hooks API talk
Silviu delivered a talk about using the Downshift
hooks at the axe-con 2021 conference. The talk, which is also
recorded, illustrates how to build an accessible select,
combobox, and support multiple selection using Downshift hooks and custom
components from ChakraUI. It offers a brief crash course to:
- build a custom Select.
- build a custom Combobox.
- enhance the Select and Combobox with multiple selection.
- use custom features like control props, the state reducer and action props.
Roadmap and contributions
Next steps:
- iterate on
useSelect,useCombobox,useMultipleSelectionto improve them. - remove the
Downshiftcomponent once the hooks are mature.
File: src/hooks/MIGRATION_V7.md
Migration from v6 to v7
Since version 7.0.0 Downshift follows the (ARIA 1.2 guideline for the
combobox)select-aria. This brought a series of changes that are considered
breaking, both to the API and the behaviour of downshift's useSelect and
useCombobox hooks. The list of changes, as well as the migration itself, is
detailed below.
Table of Contents
useSelect
Focus
Since ARIA 1.2, focus stays on the trigger element at all
times. (Previously)deprecated-select-aria, it toggled between the trigger and
the menu depending on the open state of the select element. If any of your
custom implementation involved the focus on the menu element, please change it
as the focus stays on the trigger even when the menu is open.
HTML Attributes
Similar to 1.1, useSelect communicates to the screen reader the currently
highlighted item via the aria-activedescendant attribute. However, since now
the focus is always on the trigger element, this attribute, along with others,
have shifted as shown below:
- getToggleButtonProps additions:
- role=combobox
- aria-activedescendant=${highlightedItemId}
- aria-controls=${menuId}
- tabindex=0
- getMenuProps removals:
- aria-activedescendant=${highlightedItemId}
- getItemProps changes:
- aria-selected=${item === selectedItem} - now the item that is selected
received aria-selected=true, the rest receive it as false. Previously, the
highlighted item was marked with aria-selected=true.
- aria-selected=${item === selectedItem} - now the item that is selected
Events
Event changes occured because of the focus shift, as well as new accessibility
pattern recommendantions.
getToggleButtonProps additions:
- ArrowDown+Alt: opens the menu without any item highlighted.
- ArrowUp+Alt: closes the menu and selects the highlighted item.
- End: highlights the last item and opens the menu if closed.
- Home: highlights the first item and opens the menu if closed.
- PageUp: if menu is open, moves highlight by 10 positions to the start.
- PageDown: if menu is open, moves highlight by 10 positions to the end.
- ${characterKey}: always opens the menu if closed, highlights the item
starting with that key (same behaviour as before when the menu is opened). - Enter: if menu is open, closes the menu and selects the highlighted item.
- SpaceBar: if menu is open, closes the menu and selects the highlighted
item. If the space is part of a search query, it will be added to the search
query instead. - Escape: closes the menu if open, without selecting anything.
- Tab or any other Blur: closes the menu if open, selects highlighted
item, focus moves naturally. - ArrowUp: moves highlight one position up. Shift modifier is not
supported anymore. - ArrowDown: moves highlight one position down. Shift modifier is not
supported anymore.
getToggleButtonProps changes:
- ArrowUp: if there is an item selected, opens the menu with that item
highlighted, not with the -1 offset as it did in v6 (ARIA 1.1). - ArrowDown: if there is an item selected, opens the menu with that item
highlighted, not with the +1 offset as it did in v6 (ARIA 1.1).
- ArrowUp: if there is an item selected, opens the menu with that item
getMenuProps removals:
- ArrowUp, ArrowDown, End, Home, Enter, Escape, SpaceBar, Tab.
stateChangeTypes
As a consequence of the event changes, the stateChangeTypes
received in the stateReducer and on${statePropery}Change received the
following modifications:
- MenuKeyDownArrowDown -> ToggleButtonKeyDownArrowDown
- MenuKeyDownArrowUp -> ToggleButtonKeyDownArrowUp
- MenuKeyDownEscape -> ToggleButtonKeyDownEscape
- MenuKeyDownHome -> ToggleButtonKeyDownHome
- MenuKeyDownEnd -> ToggleButtonKeyDownEnd
- MenuKeyDownEnter -> ToggleButtonKeyDownEnter
- MenuKeyDownSpaceButton -> ToggleButtonKeyDownSpaceButton
- MenuKeyDownCharacter -> ToggleButtonKeyDownCharacter
- MenuBlur -> ToggleButtonBlur
- ToggleButtonKeyDownPageUp: new state change type.
- ToggleButtonKeyDownPageDown: new state change type.
Please change your reducer / onChange code accordingly. For instance:
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.MenuKeyDownEnter:
case useSelect.stateChangeTypes.MenuKeyDownSpaceButton:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes,
isOpen: true, // keep the menu open after selection.
}
default:
return changes
}
}Becomes:
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, // keep the menu open after selection.
}
default:
return changes
}
}Another thing to mention is that, since ARIA 1.2 does not recommend a
`` element for the toggle button anymore, Enter and SpaceBar do
not perform click events out of the box, when the menu is closed.
Consequently, we are triggering these events manually. For backwards
compatibility to pre v7, when menu is closed and toggle element receives
Enter or SpaceBar key event, it will trigger auseSelect.stateChangeTypes.ToggleButtonClicktype change.
circularNavigation
The prop circularNavigation has been removed. Navigation inside the menu is
standard and non-circular. If you wish to make it circular, use the
stateReducer:
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDown:
if (state.highlightedIndex === items.length - 1) {
return {...changes, highlightedIndex: state.highlightedIndex}
} else {
return changes
}
case useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp:
if (state.highlightedIndex === 0) {
return {...changes, highlightedIndex: state.highlightedIndex}
} else {
return changes
}
default:
return changes
}
}useCombobox
HTML Attributes
The biggest change in ARIA 1.2 is that the input
wrapper element does not receive the combobox role attributes anymore.
Previouslydeprecated-combobox-aria, the role of combobox, as well as other
HTML attributes, had to be added on the input parent element. Some attributes
that belonged to the wrapper element are now added on the input element. The
changes are as follows:
- getComboboxProps has been removed.
- getInputProps additions:
- role=combobox
- aria-expanded=${isOpen}
- getToggleButtonProps additions:
- aria-controls=${menuId}
- aria-expanded=${isOpen}
Events
As a result of the 1.2 pattern, there are a few event handling changes detailed
below.
getInputProps additions:
- ArrowDown+Alt: opens the menu without any item highlighted.
- ArrowUp+Alt: closes the menu and selects the highlighted item.
- PageUp: if menu is open, moves highlight by 10 positions to the start.
- PageDown: if menu is open, moves highlight by 10 positions to the end.
- Focus: if menu is closed, opens the menu.
getInputProps changes:
- ArrowUp: moves highlight one position up. Shift modifier is not
supported anymore. - ArrowDown: moves highlight one position down. Shift modifier is not
supported anymore.
- ArrowUp: moves highlight one position up. Shift modifier is not
stateChangeTypes
As a consequence of the event changes, the stateChangeTypes
received in the stateReducer and on${statePropery}Change received the
following additions:
- InputKeyDownPageUp
- InputKeyDownPageDown
- InputFocus
You don't need to change your reducer if you want to keep the 1.2 functionality
provided by default. However, if you want to keep the menu closed when the input
gets focus, you can do:
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputFocus:
return {
...changes,
isOpen: state.isOpen, // keep the menu closed when input gets focused.
}
default:
return changes
}
}circularNavigation
The prop circularNavigation has been removed. Navigation inside the menu is
standard and circular. If you wish to make it non-circular, use the
stateReducer:
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownArrowDown:
if (state.highlightedIndex === items.length - 1) {
return {...changes, highlightedIndex: state.highlightedIndex}
}
break
case useCombobox.stateChangeTypes.InputKeyDownArrowUp:
if (state.highlightedIndex === 0) {
return {...changes, highlightedIndex: state.highlightedIndex}
}
break
default:
return changes
}
return changes
}File: src/hooks/MIGRATION_V8.md
Migration from v7 to v8
Downshift v8 receives a list of breaking changes, which are necessary to improve
both the user and the developer experience. The changes are only affecting the
hooks and are detailed below.
Table of Contents
isItemDisabled
Both useCombobox and useSelect now support the isItemDisabled function.
This new API is used to mark menu items as disabled, and as such remove the from
the navigation and prevent them from being selected. The old API required
passing the disabled prop to the getItemProps function. This old API has
been removed and you will receive a console warning if you are trying to use thedisabled prop in getItemProps.
Example of API migration:
Old:
const items = [{value: 'item1'}, {value: 'item2'}]
const {getInputProps, ...rest} = useCombobox({items})
return (
// ... rest
<li {...getItemProps({item, disabled: item.value === 'item2'})}>
)New:
const items = [{value: 'item1'}, {value: 'item2'}]
const {getInputProps, ...rest} = useCombobox({items, isItemDisabled(item, _index) { return item.value === 'item2' }})
return (
// ... rest
<li {...getItemProps({item})}>
)The API for Downshift remains unchange.
Related PR: https://github.com/downshift-js/downshift/pull/1510
useCombobox input click
ARIA 1.2 recommends to toggle the menu open state at
input click. Previously, in v7, the menu was opened on receiving focus, from
both mouse and keyboard. Starting with v8, input focus will not trigger any
state change anymore. Only the input click event will be handled and will
trigger a menu toggle. Consequently:
- getInputProps will not return any Focus event handler.
- getInputProps will return a Click event handler.
useCombobox.stateChangeTypes.InputFocushas been removed.useCombobox.stateChangeTypes.InputClickhas been added instead.
We recommend having the default toggle on input click behaviour as it's part of
the official ARIA combobox 1.2 spec, but if you wish to override it and not
toggle the menu on click, use the stateReducer:
function stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputClick:
return {
...changes,
isOpen: state.isOpen, // do not toggle the menu when input is clicked.
}
default:
return changes
}
}If you want to return to the v7 behaviour and open the menu on focus, keep the
reducer above so you remove the toggle behaviour, and call the openMenu
imperative function, returned by useCombobox, in a onFocus handler passed to
getInputProps:
<input
{...getInputProps({
onFocus() {
openMenu()
},
})}
/>Consider to use the default 1.2 ARIA behaviour provided by default in order to
align your widget to the accessibility official spec. This behaviour consistency
improves the user experience, since all comboboxes should behave the same and
there won't be need for any additional guess work done by your users.
Related PR: https://github.com/downshift-js/downshift/pull/1440
Getter props return value types
Previously, the return value from the getter props returned by both Downshift
and the hooks was any. In v8 we improved the types in order to reflect what is
actually returned: the default values return by the getter prop function and
whatever else the user passes as arguments. The type changes are done in
this PR and the
8.0.2 PR. Make sure you
adapt your TS code, if applicable.
Also, in the Downshift component, the return values for some getter prop
values have changed from null to undefined, since that is what HTML elements
expect (value or undefined). These values are also reflected in the TS types.
- getRootProps: 'aria-owns': isOpen ? this.menuId :
nullundefined, - getInputProps:
- 'aria-controls': isOpen ? this.menuId :
nullundefined - 'aria-activedescendant': isOpen && typeof highlightedIndex === 'number' &&
highlightedIndex >= 0 ? this.getItemId(highlightedIndex) :nullundefined
- 'aria-controls': isOpen ? this.menuId :
- getMenuProps: props && props['aria-label'] ?
nullundefined : this.labelId,
Related PR: https://github.com/downshift-js/downshift/pull/1482
environment propTypes
The environment prop is useful when you are using downshift in a context
different than regular DOM. Its TS type has been updated to contain Node and
the propTypes have also been updated to reflect the properties which are
required by downshift from environment.
Related PR: https://github.com/downshift-js/downshift/pull/1463
File: src/hooks/MIGRATION_V9.md
Migration from v8 to v9
Downshift v8 receives a list of breaking changes, which are necessary to improve
both the user and the developer experience. The changes are only affecting the
hooks and are detailed below.
Table of Contents
- onChange Typescript Improvements
- getA11ySelectionMessage
- getA11yRemovalMessage
- getA11yStatusMessage
- selectedItemChanged
onChange Typescript Improvements
The handlers below have their types improved to reflect that they will always
get called with their corresponding state prop:
useCombobox
- onSelectedItemChange: selectedItem is non optional
- onIsOpenChange: isOpen is non optional
- onHighlightedIndexChange: highlightedIndex is non optional
useSelect
- onSelectedItemChange: selectedItem is non optional
- onIsOpenChange: isOpen is non optional
- onHighlightedIndexChange: highlightedIndex is non optional
- onInputValueChange: inputValue is non optional
useMultipleSelection
- onActiveIndexChange: activeIndex is non optional
- onSelectedItemsChange: selectedItems is non optional
getA11ySelectionMessage
The prop has been removed from useSelect and useCombobox. If you still need an
a11y selection message, use either getA11yStatusMessage or your own aria-live
implementation inside a onStateChange callback.
getA11yRemovalMessage
The prop has been removed from useMultipleSelection. If you still need an a11y
removal message, use either getA11yStatusMessage or your own aria-live
implementation inside a onStateChange callback.
getA11yStatusMessage
The prop has been also added to useMultipleSelection, but has some changes
reflected in each of the hook's readme.
- there is no default function provided, so you will not get any aria-live
message anymore if you don't provide the prop directly to the hooks. - the function is called only with the hook's state, and you should already have
access to the props, such as items or itemToString. Values such as
highlightedItem or resultsCount have been removed, so you need to compute them
yourself if needed. Downshiftis not affected, it has the samegetA11yStatusMessageas before,
no changes there at all.
The HTML markup with the ARIA attributes we provide through the getter props
should be enough for screen readers to report:
- results count.
- highlighted item.
- item selection.
- what actions the user can take.
If you need anything more specific as part of an aria-live region, please use
the new version of getA11yStatusMessage or your own aria-live implementation.
References:
selectedItemChanged
This prop has been removed from useCombobox. You should use itemToKey
instead.
Reference:
File: src/hooks/useCombobox/README.md
useCombobox
The problem
You have a combobox or autocomplete dropdown in your application and you want it
to be accessible and functional. For consistency reasons you want it to follow
the ARIA design pattern for a combobox. You also want
this solution to be simple to use and flexible so you can tailor it further to
your specific needs.
This solution
useCombobox is a React hook that manages all the stateful logic needed to make
the combobox functional and accessible. It returns a set of props that are meant
to be called and their results destructured on the combobox's elements: its
label, toggle button, input, list and list items. The props are similar to the
ones 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 combobox 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.
Types of Autocomplete
By default, our implementation and examples illustrate an autocomplete of type
list. This involves performing your own items filtering logic as well as
keeping the aria_autocomplete value returned by the
getInputProps.
There are, in total, 3 types of autocomplete you can opt for, and these are as
follows:
- no autocomplete:
- ARIA example
- use aria-autocomplete="none" attribute to override the default value from
getInputProps. - do not implement any filtering logic yourself, just render the listbox
items. Basically, take the code example below, remove the useState
with items, the onInputValueChange function, pass colors as items prop
and render the colors if isOpen is true.
- list autocomplete:
- ARIA example
- just use the example provided below or anything equivalent.
- filtering logic inside the menu is done by the useCombobox consumer.
- list and inline autocomplete:
- ARIA example
- use aria-autocomplete="both" attribute to override the default value from
getInputProps. - filtering logic inside the menu is done by the useCombobox consumer.
- inline autocomplete based on the highlighted item in the menu is also
performed by the consumer.
Migration through breaking changes
The hook received breaking changes related to how it works, as well as the API,
starting with v7. They are documented here:
Table of Contents
- Usage
- Basic Props
- Advanced Props
- isItemDisabled
- initialSelectedItem
- initialIsOpen
- initialHighlightedIndex
- initialInputValue
- defaultSelectedItem
- defaultIsOpen
- defaultHighlightedIndex
- defaultInputValue
- itemToKey
- getA11yStatusMessage
- onHighlightedIndexChange
- onIsOpenChange
- onInputValueChange
- onStateChange
- highlightedIndex
- isOpen
- selectedItem
- inputValue
- id
- labelId
- menuId
- toggleButtonId
- inputId
- getItemId
- environment
- stateChangeTypes
- Control Props
- Returned props
- Event Handlers
- Examples
Usage
import * as React from 'react'
import {render} from 'react-dom'
import {useCombobox} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
function DropdownCombobox() {
const [inputItems, setInputItems] = React.useState(colors)
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
selectItem,
} = useCombobox({
items: inputItems,
onInputValueChange: ({inputValue}) => {
setInputItems(
colors.filter(item =>
item.toLowerCase().startsWith(inputValue.toLowerCase()),
),
)
},
})
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 'fit-content',
justifyContent: 'center',
marginTop: 100,
alignSelf: 'center',
}}
>
<label
style={{
fontWeight: 'bolder',
color: selectedItem ? selectedItem : 'black',
}}
{...getLabelProps()}
>
Choose an element:
</label>
<div>
<input
style={{padding: '4px'}}
{...getInputProps()}
data-testid="combobox-input"
/>
<button
style={{padding: '4px 8px'}}
aria-label="toggle menu"
data-testid="combobox-toggle-button"
{...getToggleButtonProps()}
>
{isOpen ? <>↑</> : <>↓</>}
</button>
<button
style={{padding: '4px 8px'}}
aria-label="toggle menu"
data-testid="clear-button"
onClick={() => selectItem(null)}
>
✗
</button>
</div>
<ul
{...getMenuProps()}
style={{
listStyle: 'none',
width: '100%',
padding: '0',
margin: '4px 0 0 0',
}}
>
{isOpen &&
inputItems.map((item, index) => (
<li
style={{
padding: '4px',
backgroundColor: highlightedIndex === index ? '#bde4ff' : null,
}}
key={`${item}${index}`}
{...getItemProps({
item,
index,
})}
>
{item}
</li>
))}
</ul>
</div>
)
}
render(<DropdownCombobox />, document.getElementById('root'))Basic Props
This is the list of props that you should probably know about. There are some
advanced props below as well.
items
any[]| required
The main difference from vanilla Downshift is that we pass the items we want
to render to the hook as well. Opening the menu with an item already selected
means the hook has to know in advance what items you plan to render and what is
the position of that item in the list. Consequently, there won't be any need for
two state changes: one for opening the menu and one for setting the highlighted
index, like in Downshift.
itemToString
function(item: any)| defaults to:item => (item ? String(item) : '')
If your items are stored as, say, objects instead of strings, downshift still
needs a string representation for each one. This is required for accessibility
aria-live messages (e.g., after making a selection).
Note: This callback must include a null check: it is invoked with null
whenever the user abandons input via <Esc>.
onSelectedItemChange
function(changes: object)| optional, no useful default
Called each time the selected item was changed. Selection can be performed by
item click, Enter Key while item is highlighted or by blurring the menu while an
item is highlighted (Tab, Shift-Tab or clicking away).
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theselectedItemproperty
with the newly selected value. This also has atypeproperty which you can
learn more about in thestateChangeTypessection. This
property will be part of the actions that can trigger aselectedItemchange,
for exampleuseCombobox.stateChangeTypes.ItemClick.
stateReducer
function(state: object, actionAndChanges: object)| optional
🚨 This is a really handy power feature 🚨
This function will be called each time useCombobox sets its internal state (or
calls your onStateChange handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates. It gives you the current state
and the state that will be set, and you return the state that you want to set.
state: The full current state of downshift.actionAndChanges: Object that contains the actiontype, thepropspassed
touseCombobox, thechangessuggested by the Downshift default reducer,
and any action-specific fields (e.g.altKey,key,index). About thetypeproperty you can learn more about in thestateChangeTypessection.
import {useCombobox} from 'downshift'
import {items} from './utils'
const {getMenuProps, getItemProps, ...rest} = useCombobox({
items,
stateReducer,
})
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
// this prevents the menu from being closed when the user selects an item with 'Enter' or mouse
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
return {
...changes, // default Downshift new state changes on item selection.
isOpen: state.isOpen, // but keep menu open.
highlightedIndex: state.highlightedIndex, // with the item highlighted.
}
default:
return changes // otherwise business as usual.
}
}NOTE: This is only called when state actually changes. You should not attempt
use this to handle events. If you wish to handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For exampleshould be). Also, your
reducer function should be "pure." This means it should do nothing other than
return the state changes you want to have happen.
Advanced Props
isItemDisabled
function(item: any, index: number)| defaults to:(_item, _index) => false
If an item needs to be marked as disabled, this function needs to return true
for that item. Disabled items will be skipped from keyboard navigation, will not
be selected and will be marked as disabled for screen readers.
initialSelectedItem
any| defaults tonull
Pass an item that should be selected when downshift is initialized.
initialIsOpen
boolean| defaults tofalse
Pass a boolean that sets the open state of the menu when downshift is
initialized.
initialHighlightedIndex
number| defaults to-1
Pass a number that sets the index of the highlighted item when downshift is
initialized.
initialInputValue
string| defaults to''
Pass a string that sets the content of the input when downshift is initialized.
defaultSelectedItem
any| defaults tonull
Pass an item that should be selected when downshift is reset.
defaultIsOpen
boolean| defaults tofalse
Pass a boolean that sets the open state of the menu when downshift is reset or
when an item is selected.
defaultHighlightedIndex
number| defaults to-1
Pass a number that sets the index of the highlighted item when downshift is
reset or when an item is selected.
defaultInputValue
string| defaults to''
Pass a string that sets the content of the input when downshift is reset or when
an item is selected.
itemToKey
function(item: any)| defaults to:item => item
Used to determine the uniqueness of an item when searching for the item or
comparing the item with another. Returns the item itself, by default, so the
comparing/searching is done internally via referential equality.
If using items as objects and their reference will change during use, you can
use the function to generate a unique key for each item, such as an id prop.
function itemToKey(item) {
return item.id
}This deprecates the "selectedItemChanged" prop. If you are using the prop
already, make sure you change to "itemToKey" as the former is removed in v9. A
migration example:
// initial items.
const items = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// the same items but with different references, for any reason.
const newItems = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// previously, if you probably had something like this.
function selectedItemChanged(item1, item2) {
return item1.id === item2.id
}
// moving forward, switch to this one.
function itemToKey(item) {
return item.id
// and we will do the comparison like: const isChanged = itemToKey(prevSelectedItem) !== itemToKey(nextSelectedItem)
}getA11yStatusMessage
function({/* see below */})| default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when the state
changes: selectedItem, highlightedIndex, inputValue or isOpen.
There is no default function provided anymore since v9, so if there's no prop
passed, no aria live status message is created. An implementation that resembles
the previous default is written below, should you want to keep pre v9 behaviour.
We don't provide this as a default anymore since we consider that screen readers
have been significantly improved and they can convey information about items
count, possible actions and highlighted items only from the HTML markup, without
the need for aria-live regions.
function getA11yStatusMessage(state) {
if (!state.isOpen) {
return ''
}
// you need to get resultCount and previousResultCount yourself now, since we don't pass them as arguments anymore
const resultCount = items.length
const previousResultCount = previousResultCountRef.current
if (!resultCount) {
return 'No results are available.'
}
if (resultCount !== previousResultCount) {
return `${resultCount} result${
resultCount === 1 ? ' is' : 's are'
} available, use up and down arrow keys to navigate. Press Enter key to select.`
}
return ''
}onHighlightedIndexChange
function(changes: object)| optional, no useful default
Called each time the highlighted item was changed. Items can be highlighted
while hovering the mouse over them or by keyboard keys such as Up Arrow, Down
Arrow, Home and End. Items can also be highlighted by hitting character keys
that are part of their starting string equivalent.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain thehighlightedIndex
property with the new value. This also has atypeproperty which you can
learn more about in thestateChangeTypessection. This
property will be part of the actions that can trigger ahighlightedIndex
change, for exampleuseCombobox.stateChangeTypes.InputKeyDownArrowUp.
onIsOpenChange
function(changes: object)| optional, no useful default
Called each time the menu is open or closed. Menu can be open by toggle button
click, Enter, Space, Up Arrow or Down Arrow keys. Can be closed by selecting an
item, blur (Tab, Shift-Tab or clicking outside), clicking the toggle button
again or hitting Escape key.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theisOpenproperty with
the new value. This also has atypeproperty which you can learn more about
in thestateChangeTypessection. This property will be
part of the actions that can trigger aisOpenchange, for exampleuseCombobox.stateChangeTypes.ToggleButtonClick.
onInputValueChange
function(changes: object)| optional, no useful default
Called each time the value in the input text changes. The input value should
change like any input of type text, at any character key press, Space,Backspace, Escape etc.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theinputValueproperty
with the new value. This also has atypeproperty which you can learn more
about in thestateChangeTypessection. This property
will be part of the actions that can trigger ainputValuechange, for
exampleuseCombobox.stateChangeTypes.InputChange.
onStateChange
function(changes: object)| optional, no useful default
This function is called anytime the internal state changes. This can be useful
if you're using downshift as a "controlled" component, where you manage some or
all of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then
pass it as props, rather than letting downshift control all its state itself.
changes: These are the properties that actually have changed since the last
state change. This also has atypeproperty which you can learn more about
in thestateChangeTypessection.
Tip: This function will be called any time any state is changed. The best
way to determine whether any particular state was changed, you can usechanges.hasOwnProperty('propName')or use theon[statePropKey]Changeprops
described above.
NOTE: This is only called when state actually changes. You should not attempt
to use this to handle events. If you wish handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For example:should be).
highlightedIndex
number| control prop (read more about this in
the Control Props section)
The index of the item that should be highlighted when menu is open.
isOpen
boolean| control prop (read more about this in
the Control Props section)
The open state of the menu.
selectedItem
any| control prop (read more about this in
the Control Props section)
The item that should be selected.
inputValue
string| control prop (read more about this in
the Control Props section)
The value to be displayed in the text input.
🚨 Important 🚨
If you use onInputValueChange, onStateChange or anything similar in order to
update a state variable that will end up controlling inputValue, you will
encounter a
cursor jump issue.
There's no way to properly fix this in our current React.useReducer setup, so
in order to work around the issue, consider the change below.
const [value, setValue] = useState('')
const {getInputProps} = useCombobox({
items: [],
inputValue: value,
// change this:
onInputValueChange: ({inputValue}) => {
setValue(inputValue)
},
})
return (
<input
{...getInputProps({
// to this:
onChange: e => {
setValue(e.target.value)
},
})}
/>
)id
string| defaults to a generated ID
Used to generate the first part of the Downshift id on the elements. You can
override this id with one of your own, provided as a prop, or you can override
the id for each element altogether using the props below.
labelId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (label) you usegetLabelProps with.
menuId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (ul) you usegetMenuProps with.
toggleButtonId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (button) you usegetToggleButtonProps with.
inputId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (input) you usegetInputProps with.
getItemId
function(index)| defaults to a function that generates an ID based on the
index
Used for aria attributes and the id prop of the element (li) you usegetItemProps with.
environment
window| defaults towindow
This prop is only useful if you're rendering downshift within a differentwindow context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/oradd|removeEventListener on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
access to these properties
for downshift.
stateChangeTypes
There are a few props that expose changes to state
(onStateChange and stateReducer). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a type property on the changes
object you get. This type corresponds to a stateChangeTypes property.
The list of all possible values this type property can take is defined in
this file and is as follows:
useCombobox.stateChangeTypes.InputKeyDownArrowDownuseCombobox.stateChangeTypes.InputKeyDownArrowUpuseCombobox.stateChangeTypes.InputKeyDownEscapeuseCombobox.stateChangeTypes.InputKeyDownHomeuseCombobox.stateChangeTypes.InputKeyDownEnduseCombobox.stateChangeTypes.InputKeyDownPageUpuseCombobox.stateChangeTypes.InputKeyDownPadeDownuseCombobox.stateChangeTypes.InputKeyDownEnteruseCombobox.stateChangeTypes.InputChangeuseCombobox.stateChangeTypes.InputClickuseCombobox.stateChangeTypes.InputBluruseCombobox.stateChangeTypes.MenuMouseLeaveuseCombobox.stateChangeTypes.ItemMouseMoveuseCombobox.stateChangeTypes.ItemClickuseCombobox.stateChangeTypes.ToggleButtonClickuseCombobox.stateChangeTypes.FunctionToggleMenuuseCombobox.stateChangeTypes.FunctionOpenMenuuseCombobox.stateChangeTypes.FunctionCloseMenuuseCombobox.stateChangeTypes.FunctionSetHighlightedIndexuseCombobox.stateChangeTypes.FunctionSelectItemuseCombobox.stateChangeTypes.FunctionSetInputValueuseCombobox.stateChangeTypes.FunctionReset
See stateReducer for a concrete example on how to use thetype property.
Control Props
Downshift manages its own state internally and calls youronSelectedItemChange, onIsOpenChange, onHighlightedIndexChange,onInputChange and onStateChange handlers with any relevant changes. The
state that downshift manages includes: isOpen, selectedItem, inputValue
and highlightedIndex. Returned action function (read more below) can be used
to manipulate this state and can likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon asthis.props[statePropKey] !== undefined, internally, downshift will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where onStateChange
comes in really handy), but you can also control the state from anywhere, be
that state from other components, redux, react-router, or anywhere else.
Note: This is very similar to how normal controlled components work elsewhere
in react (like ``). If you want to learn more about this concept, you
can learn about that from the Advanced React Component Patterns
course
Returned props
You use the hook like so:
import {useCombobox} from 'downshift'
import {items} from './utils'
const {getInputProps, reset, ...rest} = useCombobox({
items,
...otherProps,
})
return (
<div>
<input {...getInputProps()} />
{/* render the menu and items */}
{/* render a button that resets the select to defaults */}
<button
onClick={() => {
reset()
}}
>
Reset
</button>
</div>
)NOTE: In this example we used both a getter prop
getInputPropsand an action
propreset. The properties ofuseComboboxcan be split into three
categories as indicated below:
prop getters
NOTE: These prop-getters provide
aria-attributes which are very important
to your component being accessible. It's recommended that you utilize these
functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question, for example on the toggle button:
` Note: For accessibility purposes, calling this method is highly recommended.
`getMenuProps`
This method should be applied to the element which contains your list of items.
Typically, this will be a or a that surrounds a map expression.
This handles the proper ARIA roles and attributes.
Optional properties:
refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getMenuProps({refKey: 'innerRef'})and
your composite component would forward like:. However, if you are just rendering a primitive component like, there
is no need to specify this property. It defaults toref.Please keep in mind that menus, for accessiblity purposes, should always be
rendered, regardless of whether you hide it or not. Otherwise,getMenuProps
may throw error if you unmount and remount the menu.aria-label: By default the menu will add anaria-labelledbythat refers to
the<label>rendered withgetLabelProps. However, if you providearia-labelto give a more specific label that describes the options
available, thenaria-labelledbywill not be provided and screen readers can
use youraria-labelinstead.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetMenuProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useCombobox will
unexpectedly fail.
const {getMenuProps} = useCombobox({items})
const ui = (
<ul {...getMenuProps()}>
{!isOpen
? null
: items.map((item, index) => (
<li {...getItemProps({item, index, key: item.id})}>{item.name}</li>
))}
</ul>
)Note that for accessibility reasons it's best if you always render this
element whether or not downshift is in anisOpenstate.
`getItemProps`
The props returned from calling this function should be applied to any menu
items you render.
This is an impure function, so it should only be called when you will
actually be applying the props to an item.
What do you mean by impure function?
Basically just don't do this:
items.map((item, index) => {
const props = getItemProps({item, index}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return <div {...props} />
})Instead, you could do this:
items.filter(shouldRenderItem).map(item => <div {...getItemProps({item})} />)Required properties:
The main difference from vanilla Downshift is that we require the items as
props before rendering. The reason is to open the menu with items already
highlighted, and we need to know the items before the actual render. It is still
required to pass either item or index to getItemProps.
item: this is the item data that will be selected when the user selects a
particular item.index: This is howdownshiftkeeps track of your item when updating thehighlightedIndexas the user keys around. By default,downshiftwill
assume theindexis the order in which you're callinggetItemProps. This
is often good enough, but if you find odd behavior, try setting this
explicitly. It's probably best to be explicit aboutindexwhen using a
windowing library likereact-virtualized.
Optional properties:
ref: if you need to access the item element via a ref object, you'd call the
function like this:getItemProps({ref: yourItemRef}). As a result, the item
element will receive a composedrefproperty, which guarantees that both
your code anduseComboboxuse the same correct reference to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getItemProps({refKey: 'innerRef'})and
your composite component would forward like:<li ref={props.innerRef} />.
However, if you are just rendering a primitive component like ``, there
is no need to specify this property. It defaults toref.
`getToggleButtonProps`
Call this and apply the returned props to a button. It allows you to toggle
the Menu component.
Optional properties:
ref: if you need to access the button element via a ref object, you'd call
the function like this:getToggleButton({ref: yourButtonRef}). As a result,
the button element will receive a composedrefproperty, which guarantees
that both your code anduseComboboxuse the same correct reference to the
element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getToggleButton({refKey: 'innerRef'})
and your composite component would forward like:. However, if you are just rendering a primitive component like, there is no need to specify this property.
It defaults toref.disabled: If this is set totrue, then all of the downshift button event
handlers will be omitted (it won't toggle the menu when clicked).
const {getToggleButtonProps} = useCombobox({items})
const myButton = (
<button {...getToggleButtonProps()}>Click me</button>
{/* menu and items */}
)`getInputProps`
This method should be applied to the input you render. It is recommended that
you pass all props as an object to this method which will compose together any
of the event handlers you need to apply to the input while preserving the ones
that downshift needs to apply to make the input behave.
There are no required properties for this method.
Optional properties:
disabled: If this is set to true, then no event handlers will be returned
fromgetInputPropsand adisabledprop will be returned (effectively
disabling the input).ref: if you need to access the input element via a ref object, you'd call
the function like this:getInputProps({ref: yourInputRef}). As a result, the
input element will receive a composedrefproperty, which guarantees that
both your code anduseComboboxuse the same correct reference to the
element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getInputProps({refKey: 'innerRef'})and
your composite component would forward like:. However, if you are just rendering a primitive component like, there
is no need to specify this property. It defaults toref.aria-label: By default the input will add anaria-labelledbythat refers
to the<label>rendered withgetLabelProps. However, if you providearia-labelto give a more specific label that describes the options
available, thenaria-labelledbywill not be provided and screen readers can
use youraria-labelinstead.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetInput. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useCombobox will
unexpectedly fail.
actions
These are functions you can call to change the state of the downshiftuseCombobox hook.
| property | type | description |
|---|---|---|
closeMenu |
function() |
closes the menu |
openMenu |
function() |
opens the menu |
selectItem |
function(item: any) |
selects the given item |
setHighlightedIndex |
function(index: number) |
call to set a new highlighted index |
setInputValue |
function(value: string) |
call to set a new value in the input |
toggleMenu |
function() |
toggle the menu open state |
reset |
function() |
this resets downshift's state to a reasonable default |
state
These are values that represent the current state of the downshift component.
| property | type | description |
|---|---|---|
highlightedIndex |
number |
the currently highlighted item |
isOpen |
boolean |
the menu open state |
selectedItem |
any |
the currently selected item input |
inputValue |
string |
the value in the input |
Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call event.preventDefault(). Their additional functionality is
described below.
Default handlers
Toggle Button
Click: If the menu is not displayed, it will open it. Otherwise it will
close it. It will additionally move focus on the input in order for screen
readers to correctly narrate which item is currently highlighted. If there is
already an item selected, the menu will be opened with that item already
highlighted.Enter: Has the same effect asClick. Button not in the tab order by
default.Space: Has the same effect asClick. Button not in the tab order by
default.
Input
ArrowDown: MoveshighlightedIndexone position down. When reaching the
last option,ArrowDownwill movehighlightedIndexto first position.ArrowUp: MoveshighlightedIndexone position up. When reaching the first
option,ArrowUpwill movehighlightedIndexto last position.Alt+ArrowDown: If the menu is closed, it will open it, without highlighting
any item.Alt+ArrowUp: If the menu is open, it will close it and will select the item
that was highlighted.CharacterKey: Will change theinputValueaccording to the value visible in
the ``.BackspaceorSpacetrigger the same event.End: If the menu is open, it will highlight the last item in the list.Home: If the menu is open, it will highlight the first item in the list.PageUp: If the menu is open, it will move the highlight the item 10
positions before the current selection.PageDown: If the menu is open, it will move the highlight the item 10
positions after the current selection.Enter: If there is a highlighted option, it will select it and close the
menu.Escape: It will close the menu if open. If the menu is closed, it will clear
selection: the value in theinputwill become an empty string and the item
stored asselectedItemwill becomenull.Click: If the menu is closed, it will open it. If the menu is open, it will
close it.Blur(Tab, Shift+Tab): It will close the menu and select the highlighted
item, if any. The focus will move naturally to the next/previous element in
the Tab order.Blur(mouse click outside): It will close the menu without selecting any
element, even if there is one highlighted.
Menu
MouseLeave: Will clear the value of thehighlightedIndexif it was set.
Item
Click: It will select the item, close the menu and move focus to the toggle
button (unlessdefaultIsOpenis true).MouseOver: It will highlight the item.
Customizing Handlers
You can provide your own event handlers to useCombobox which will be called
before the default handlers:
const items = [...] // items here.
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps({
onKeyDown: event => {
// your custom keyDown handler here.
},
})}
/>
)If you would like to prevent the default handler behavior in some cases, you can
set the event's preventDownshiftDefault property to true:
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps({
onKeyDown: event => {
// your custom keyDown handler here.
if (event.key === 'Enter') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
})}
/>
)If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
const items = [...] // items here.
const {getMenuProps} = useCombobox({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps()}
onKeyDown={event => {
// your custom keyDown handler here.
}}
/>
)Examples
Usage examples are kept on the downshift docsite and also on the
sandbox repo. Each example has a link to its own Codesandbox
version, so check the docs.
It can be a great contributing opportunity to provide relevant use cases as
docsite examples. If you have such an example, please create an issue with the
suggestion and the Codesandbox for it, and we will take it from there.
File: src/hooks/useMultipleSelection/README.md
useMultipleSelection
Deprecated: migration to useTagGroup
If you're considering a multiple selection solution with tags, check
useTagGroup.
If you're already using the hook, migrate to useTagGroup. It's meant to offer
a more intuitive and extensible API than useMultipleSelection. The
migration-guide shows how to migrate to the new hook.
The problem
You have a custom select or a combobox in your applications which performs a
multiple selection. You want want the whole experience to be accessible,
including adding and removing items from selection, navigating between the items
and back to the dropdown. You also want this solution to be simple to use and
flexible so you can tailor it further to your specific needs.
This solution
useMultipleSelection is a React hook that manages all the stateful logic
needed to make the multiple selection dropdown functional and accessible. It
returns a set of props that are meant to be called and their results
destructured on the dropdown's elements that involve the multiple selection
experience: the dropdown main element itself, which can be either an input (if
you are building a combobox) or a button (if you are building a select),
and the selected items. The props are similar to the ones provided by vanillaDownshift 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
become accessible. Every functionality needed should be provided out-of-the-box:
arrow navigation between dropdown and items, navigation between the items
themselves, removing and adding items, and also helpful aria-live messages
such as when an item has been removed from selection.
Migration through breaking changes
The hook received breaking changes related to how it works, as well as the API,
starting with v8. They are documented here:
Table of Contents
- Usage
- Basic Props
- Advanced Props
- stateChangeTypes
- Control Props
- Returned props
- Event Handlers
- Examples
Usage
import * as React from 'react'
import {render} from 'react-dom'
import {useCombobox, useMultipleSelection} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
const initialSelectedItems = [colors[0], colors[1]]
function getFilteredItems(selectedItems, inputValue) {
const lowerCasedInputValue = inputValue.toLowerCase()
return colors.filter(
colour =>
!selectedItems.includes(colour) &&
colour.toLowerCase().startsWith(lowerCasedInputValue),
)
}
function DropdownMultipleCombobox() {
const [inputValue, setInputValue] = React.useState('')
const [selectedItems, setSelectedItems] = React.useState(initialSelectedItems)
const items = React.useMemo(
() => getFilteredItems(selectedItems, inputValue),
[selectedItems, inputValue],
)
const {getSelectedItemProps, getDropdownProps, removeSelectedItem} =
useMultipleSelection({
selectedItems,
onStateChange({selectedItems: newSelectedItems, type}) {
switch (type) {
case useMultipleSelection.stateChangeTypes
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
setSelectedItems(newSelectedItems)
break
default:
break
}
},
})
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
clearSelection,
} = useCombobox({
items,
inputValue,
selectedItem: null,
stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
...(changes.selectedItem && {isOpen: true, highlightedIndex: 0}),
}
default:
return changes
}
},
onStateChange({
inputValue: newInputValue,
type,
selectedItem: newSelectedItem,
}) {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
setSelectedItems([...selectedItems, newSelectedItem])
break
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue)
break
default:
break
}
},
})
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 'fit-content',
justifyContent: 'center',
marginTop: 100,
alignSelf: 'center',
}}
>
<label
style={{
fontWeight: 'bolder',
color: selectedItem ? selectedItem : 'black',
}}
{...getLabelProps()}
>
Choose an element:
</label>
<div
style={{
display: 'inline-flex',
gap: '8px',
alignItems: 'center',
flexWrap: 'wrap',
padding: '6px',
}}
>
{selectedItems.map(
function renderSelectedItem(selectedItemForRender, index) {
return (
<span
style={{
backgroundColor: 'lightgray',
paddingLeft: '4px',
paddingRight: '4px',
borderRadius: '6px',
}}
key={`selected-item-${index}`}
{...getSelectedItemProps({
selectedItem: selectedItemForRender,
index,
})}
>
{selectedItemForRender}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<span
style={{padding: '4px', cursor: 'pointer'}}
onClick={e => {
e.stopPropagation()
removeSelectedItem(null)
}}
>
✕
</span>
</span>
)
},
)}
<div>
<input
style={{padding: '4px'}}
{...getInputProps(getDropdownProps({preventKeyAction: isOpen}))}
data-testid="combobox-input"
/>
<button
style={{padding: '4px 8px'}}
aria-label="toggle menu"
data-testid="combobox-toggle-button"
{...getToggleButtonProps()}
>
{isOpen ? <>↑</> : <>↓</>}
</button>
<button
style={{padding: '4px 8px'}}
aria-label="clear selection"
data-testid="clear-button"
onClick={clearSelection}
>
✗
</button>
</div>
</div>
<ul
{...getMenuProps()}
style={{
listStyle: 'none',
width: '100%',
padding: '0',
margin: '4px 0 0 0',
}}
>
{isOpen &&
items.map((item, index) => (
<li
style={{
padding: '4px',
backgroundColor: highlightedIndex === index ? '#bde4ff' : null,
}}
key={`${item}${index}`}
{...getItemProps({
item,
index,
'data-testid': `downshift-item-${index}`,
})}
>
{item}
</li>
))}
</ul>
</div>
)
}
render(<DropdownMultipleCombobox />, document.getElementById('root'))The equivalent example with useSelect is here.
Basic Props
This is the list of props that you should probably know about. There are some
advanced props below as well.
onSelectedItemsChange
function(changes: object)| optional, no useful default
Called each time the selected items array changes. Especially useful when items
are removed, as there are many ways to do that: Backspace from dropdown,Backspace or Delete while focus is the item, executing removeSelectedItem
when clicking an associated X icon for the item.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theselectedItems
property with the new array value. This also has atypeproperty which you
can learn more about in thestateChangeTypessection.
This property will be part of the actions that can trigger anselectedItems
change, for exampleuseSelect.stateChangeTypes.DropdownKeyDownBackspace.
stateReducer
function(state: object, actionAndChanges: object)| optional
🚨 This is a really handy power feature 🚨
This function will be called each time useMultipleSelection sets its internal
state (or calls your onStateChange handler for control props). It allows you
to modify the state change that will take place which can give you fine grain
control over how the component interacts with user updates. It gives you the
current state and the state that will be set, and you return the state that you
want to set.
state: The full current state of downshift.actionAndChanges: Object that contains the actiontype, props needed to
return a new state based on that type and the changes suggested by the
Downshift default reducer. About thetypeproperty you can learn more about
in thestateChangeTypessection.
import {useMultipleSelection} from 'downshift'
import {items} from './utils'
const {getDropdownProps, getSelectedItemProps, ...rest} = useMultipleSelection({
initialSelectedItems: [items[0], items[1]],
stateReducer,
})
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
// this adds focus to the dropdown when item is removed by keyboard action.
switch (type) {
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
return {
...changes,
activeIndex: -1, // the focus will move to the input/button
}
default:
return changes // otherwise business as usual.
}
}NOTE: This is only called when state actually changes. You should not attempt
use this to handle events. If you wish to handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For exampleshould be). Also, your
reducer function should be "pure." This means it should do nothing other than
return the state changes you want to have happen.
Advanced Props
keyNavigationNext
string| defaults toArrowRight
The navigation key that increments activeIndex and moves focus to the selected
item whose index corresponds to the new value. For a RTL scenario, a common
overriden value could be ArrowLeft. In some scenarios it can be ArrowDown.
It mostly depends on the UI the user is presented with.
keyNavigationPrevious
string| defaults toArrowLeft
The navigation key that decrements activeIndex and moves focus to the selected
item whose index corresponds to the new value. Also moves focus from dropdown
to item with the last index. For a RTL scenario, a common overriden value
could be ArrowRight. In some scenarios it can be ArrowUp. It mostly depends
on the UI the user is presented with.
initialSelectedItems
any[]| defaults to[]
Pass an initial array of items that are considered to be selected.
initialActiveIndex
number| defaults to-1
Pass a number that sets the index of the focused / active selected item when
downshift is initialized.
defaultSelectedItems
any[]| defaults to[]
Pass an array of items that are going to be used when downshift is reset.
defaultActiveIndex
number| defaults to-1
Pass a number that sets the index of the focused / active selected item when
downshift is reset.
itemToKey
function(item: any)| defaults to:item => item
Used to determine the uniqueness of an item when searching for the item or
comparing the item with another. Returns the item itself, by default, so the
comparing/searching is done internally via referential equality.
If using items as objects and their reference will change during use, you can
use the function to generate a unique key for each item, such as an id prop.
// initial items.
const selectedItems = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// the same items but with different references, for any reason.
const newSelectedItems = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
function itemToKey(item) {
return item.id
// and we will do the comparison like: const isChanged = itemToKey(prevSelectedItem) !== itemToKey(nextSelectedItem)
}getA11yStatusMessage
function({/* see below */})| default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when the state
changes: selectedItem, highlightedIndex, inputValue or isOpen.
There is no default function provided anymore since v9, so if there's no prop
passed, no aria live status message is created. An implementation that resembles
the previous default is written below, should you want to keep pre v9 behaviour.
We don't provide this as a default anymore since we consider that screen readers
have been significantly improved and they can convey information about items
count, possible actions and highlighted items only from the HTML markup, without
the need for aria-live regions.
function getA11yStatusMessage(state) {
const {selectedItems} = state
if (selectedItems.length === previousSelectedItemsRef.current.length) {
return ''
}
const removedSelectedItem = previousSelectedItemsRef.current.find(
selectedItem =>
selectedItems.findIndex(
item => props.itemToKey(item) === props.itemToKey(selectedItem),
) < 0,
)
// where itemToString is a function that returns the string equivalent for an item.
return `${itemToString(removedSelectedItem)} has been removed.`
}onActiveIndexChange
function(changes: object)| optional, no useful default
Called each time the index of the active item changes. When an item becomes
active, it receives focus, so it can receive keyboard events. To changeactiveIndex you can either click on the item or use navigation keys between
the items and the dropdown.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theactiveIndexproperty
with the new value. This also has atypeproperty which you can learn more
about in thestateChangeTypessection. This property
will be part of the actions that can trigger aactiveIndexchange, for
exampleuseSelect.stateChangeTypes.ItemClick.
onStateChange
function(changes: object)| optional, no useful default
This function is called anytime the internal state changes. This can be useful
if you're using downshift as a "controlled" component, where you manage some or
all of the state (e.g. selectedItems and activeIndex) and then pass it as props,
rather than letting downshift control all its state itself.
changes: These are the properties that actually have changed since the last
state change. This also has atypeproperty which you can learn more about
in thestateChangeTypessection.
Tip: This function will be called any time any state is changed. The best
way to determine whether any particular state was changed, you can usechanges.hasOwnProperty('propName')or use theon[statePropKey]Changeprops
described above.
NOTE: This is only called when state actually changes. You should not attempt
to use this to handle events. If you wish handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For example:should be).
activeIndex
number| control prop (read more about this in
the Control Props section)
The index of the item that should be active and focused.
selectedItems
any[]| control prop (read more about this in
the Control Props section)
The items that are considered selected at the time.
environment
window| defaults towindow
This prop is only useful if you're rendering downshift within a differentwindow context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/oradd|removeEventListener on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
access to these properties
for downshift.
stateChangeTypes
There are a few props that expose changes to state
(onStateChange and stateReducer). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a type property on the changes
object you get. This type corresponds to a stateChangeTypes property.
The list of all possible values this type property can take is defined in
this file and is as follows:
useMultipleSelection.stateChangeTypes.SelectedItemClickuseMultipleSelection.stateChangeTypes.SelectedItemKeyDownDeleteuseMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspaceuseMultipleSelection.stateChangeTypes.SelectedItemKeyDownNavigationNextuseMultipleSelection.stateChangeTypes.SelectedItemKeyDownNavigationPrevioususeMultipleSelection.stateChangeTypes.DropdownKeyDownNavigationPrevioususeMultipleSelection.stateChangeTypes.DropdownKeyDownBackspaceuseMultipleSelection.stateChangeTypes.DropdownClickuseMultipleSelection.stateChangeTypes.FunctionAddSelectedItemuseMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItemuseMultipleSelection.stateChangeTypes.FunctionSetSelectedItemsuseMultipleSelection.stateChangeTypes.FunctionSetActiveIndexuseMultipleSelection.stateChangeTypes.FunctionReset
See stateReducer for a concrete example on how to use thetype property.
Control Props
Downshift manages its own state internally and calls youronSelectedItemsChange, onActiveIndexChange and onStateChange handlers with
any relevant changes. The state that downshift manages includes: selectedItems
and activeIndex. Returned action function (read more below) can be used to
manipulate this state and can likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon asthis.props[statePropKey] !== undefined, internally, downshift will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where onStateChange
comes in really handy), but you can also control the state from anywhere, be
that state from other components, redux, react-router, or anywhere else.
Note: This is very similar to how normal controlled components work elsewhere
in react (like ``). If you want to learn more about this concept, you
can learn about that from this the Advanced React Component Patterns
course
Returned props
You use the hook like so:
import {useMultipleSelection} from 'downshift'
import {items} from './utils'
const {getDropdownProps, getSelectedItemProps, selectedItems, reset, ...rest} =
useMultipleSelection({
initialSelectedItems: [items[0], [items[1]]],
...otherProps,
})
return (
<div>
{/* render the selected items */}
{selectedItems.map((selectedItem, index) => (
<span
key={`selected-item-${index}`}
{...getSelectedItemProps({selectedItem, index})}
>
{selectedItem}
</span>
))}
{/* render the dropdown itself, with getToggleButtonProps coming from useSelect if you choose to build a <select> with useSelect */}
<button {...getDropdownProps(getToggleButtonProps())}>
Select options
</button>
{/* render the menu and items */}
{/* render a button that resets the select to defaults */}
<button
onClick={() => {
reset()
}}
>
Reset
</button>
</div>
)NOTE: In this example we used both the getter props
getSelectedItemPropsandgetDropdownPropsand an action propreset. The properties ofuseMultipleSelectioncan be split into three categories as indicated below:
prop getters
NOTE: These prop-getters provide
aria-attributes which are very important
to your component being accessible. It's recommended that you utilize these
functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question, for example on the toggle button:
`
What do you mean by impure function?
Basically just don't do this:
selectedItems.map((selectedItem, index) => {
const props = getSelectedItemProps({selectedItem, index}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return <div {...props} />
})Instead, you could do this:
selectedItems
.filter(shouldRenderItem)
.map(selectedItem => <div {...getSelectedItemProps({selectedItem})} />)Required properties:
It is required to pass either selectedItem or index togetSelectedItemProps in order to be able to apply the activeIndex logic.
selectedItem: this is the item data that will be selected when the user
selects a particular item.index: This is howdownshiftkeeps track of your item when updating theactiveIndexas the user keys around. By default,downshiftwill assume theindexis the order in which you're callinggetSelectedItemProps. This is
often good enough, but if you find odd behavior, try setting this explicitly.
It's probably best to be explicit aboutindexwhen using a windowing library
likereact-virtualized.
Optional properties:
ref: if you need to access the dropdown element via a ref object, you'd call
the function like this:getDropdown({ref: yourDropdownRef}). As a result,
the dropdown element will receive a composedrefproperty, which guarantees
that both your code anduseMultipleSelectionuse the same correct reference
to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getSelectedItemProps({refKey: 'innerRef'})and your composite component
would forward like:<li ref={props.innerRef} />. However, if you are just
rendering a primitive component like ``, there is no need to specify this
property. It defaults toref.
`getDropdownProps`
Call this and apply the returned props to a button if you are building aselect or to an input if you're building a combobox. It allows you to move
focus from this element to the last item selected by using ArrowLeft and also
to remove the last item using Backspace.
Optional properties:
preventKeyAction: tellsuseMultipleSelectionifdropdownis allowed to
executedownshifthandlers onkeydown. For example, you can passisOpen
as value and user will not be able to delete selecteditems byBackspaceor
to navigate to them by arrow keys. This is useful if you don't want to mix key
actions from multiple selection with the ones from the dropdown. Once the
dropdown is closed then deletion / navigation can be resumed for multiple
selection. The value isfalseby default.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getDropdownProps({refKey: 'innerRef'})
and your composite component would forward like:. However, if you are just rendering a primitive component like, there is no need to specify this property.
It defaults toref.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetDropdownProps. Please use it with extreme care and only if you are
absolutely sure that the ref is correctly forwarded otherwiseuseMultipleSelection will unexpectedly fail.
const {getDropdownProps} = useMultipleSelection()
const {isOpen, ...rest} = useSelect({items})
const myButton = (
{/* selected items */}
<button {...getDropdownProps({preventKeyAction: isOpen})}>Click me</button>
{/* menu and items */}
)actions
These are functions you can call to change the state of the downshiftuseMultipleSelection hook.
| property | type | description |
|---|---|---|
addSelectedItem |
function(item: any) |
adds an item to the selected array |
removeSelectedItem |
function(item: any) |
removes an item from the selected array |
reset |
function() |
resets the selectedItems and active index to defaults |
setActiveIndex |
function(index: number) |
sets activeIndex to the new value |
setSelectedItems |
function(items: any[]) |
sets selectedItems to the new value |
state
These are values that represent the current state of the downshift component.
| property | type | description |
|---|---|---|
activeIndex |
number |
the index of thecurrently active item |
selectedItems |
any[] |
the items of the selection |
Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call event.preventDefault(). Their additional functionality is
described below.
Default handlers
Dropdown - button or input
ArrowLeft: Moves focus frombutton/inputto the last selected item and
makesactiveIndexto beselectedItems.length - 1. Performs this action if
there are any items selected.ArrowLeftcan be overriden with any other key
depeding on the requirements. More info onkeyNavigationPrevious.Backspace: Removes the last selected item from selection. It always performs
this action on a non-input element. If thedropdownis acomboboxthe text
cursor of theinputmust be at the start of theinputand not highlight
any text in order for the removal to work.
Item
Click: It will make the item active, will modifyactiveIndexto reflect
the new change, and will add focus to that item.Delete: It will remove the item from selection.activeIndexwill stay the
same if the item removed was not the last one, but focus will move to the item
which now has that index. If the last item was removed, theactiveIndexwill
decrease by one and will also move focus to the corresponding item. If there
are no items available anymore, the focus moves to the dropdown andactiveIndexbecomes-1.Backspace: Same effect asDelete.ArrowLeft: MovesactiveIndexand focus to previous item. It stops at the
first item in the selection.ArrowLeftcan be overriden with any other key
depeding on the requirements. More info onkeyNavigationPrevious.ArrowRight: MovesactiveIndexand focus to next item. It will move focus
to thedropdownif it occurs on the last selected item.ArrowRightcan be
overriden with any other key depeding on the requirements. More info onkeyNavigationNext.
Customizing Handlers
You can provide your own event handlers to useMultipleSelection which will be
called before the default handlers:
const items = [...] // items here.
const {getDropdownProps} = useMultipleSelection()
const {getInputProps} = useCombobox({items})
const ui = (
/* label, selected items, ... */
<input
{...getInputProps(
getDropdownProps({
onKeyDown: event => {
// your custom keyDown handler here.
},
}),
)}
/>
)If you would like to prevent the default handler behavior in some cases, you can
set the event's preventDownshiftDefault property to true:
const items = [...] // items here.
const {getDropdownProps} = useMultipleSelection()
const {getInputProps} = useCombobox({items})
const ui = (
/* label, selected items, ... */
<input
{...getInputProps(
getDropdownProps({
onKeyDown: event => {
// your custom keyDown handler here.
if (event.key === 'Enter') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
}),
)}
/>
)If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
const items = [...] // items here.
const {getDropdownProps} = useMultipleSelection()
const {getInputProps} = useCombobox({items})
const ui = (
/* label, selected items, ... */
<input
{...getInputProps(
getDropdownProps({
onKeyDown: event => {
// your custom keyDown handler here.
},
}),
)}
onKeyDown={event => {
// your custom keyDown handler here.
}}
/>
)Examples
Usage examples are kept on the downshift docsite and also on the
sandbox repo. Each example has a link to its own Codesandbox
version, so check the docs.
It can be a great contributing opportunity to provide relevant use cases as
docsite examples. If you have such an example, please create an issue with the
suggestion and the Codesandbox for it, and we will take it from there.
File: src/hooks/useMultipleSelection/MIGRATION_GUIDE.md
useMultipleSelection to useTagGroup
Usage examples
Let's consider the following usage examples:
import * as React from 'react'
import {render} from 'react-dom'
import {useCombobox, useMultipleSelection} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
const initialSelectedItems = [colors[0], colors[1]]
function getFilteredItems(selectedItems, inputValue) {
const lowerCasedInputValue = inputValue.toLowerCase()
return colors.filter(
colour =>
!selectedItems.includes(colour) &&
colour.toLowerCase().startsWith(lowerCasedInputValue),
)
}
function DropdownMultipleCombobox() {
const [inputValue, setInputValue] = React.useState('')
const [selectedItems, setSelectedItems] = React.useState(initialSelectedItems)
const items = React.useMemo(
() => getFilteredItems(selectedItems, inputValue),
[selectedItems, inputValue],
)
const {getSelectedItemProps, getDropdownProps, removeSelectedItem} =
useMultipleSelection({
selectedItems,
onStateChange({selectedItems: newSelectedItems, type}) {
switch (type) {
case useMultipleSelection.stateChangeTypes
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
setSelectedItems(newSelectedItems)
break
default:
break
}
},
})
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
selectedItem,
clearSelection,
} = useCombobox({
items,
inputValue,
selectedItem: null,
stateReducer(state, actionAndChanges) {
const {changes, type} = actionAndChanges
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
case useCombobox.stateChangeTypes.InputBlur:
return {
...changes,
...(changes.selectedItem && {isOpen: true, highlightedIndex: 0}),
}
default:
return changes
}
},
onStateChange({
inputValue: newInputValue,
type,
selectedItem: newSelectedItem,
}) {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
setSelectedItems([...selectedItems, newSelectedItem])
break
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue)
break
default:
break
}
},
})
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 'fit-content',
justifyContent: 'center',
marginTop: 100,
alignSelf: 'center',
}}
>
<label
style={{
fontWeight: 'bolder',
color: selectedItem ? selectedItem : 'black',
}}
{...getLabelProps()}
>
Choose an element:
</label>
<div
style={{
display: 'inline-flex',
gap: '8px',
alignItems: 'center',
flexWrap: 'wrap',
padding: '6px',
}}
>
{selectedItems.map(
function renderSelectedItem(selectedItemForRender, index) {
return (
<span
style={{
backgroundColor: 'lightgray',
paddingLeft: '4px',
paddingRight: '4px',
borderRadius: '6px',
}}
key={`selected-item-${index}`}
{...getSelectedItemProps({
selectedItem: selectedItemForRender,
index,
})}
>
{selectedItemForRender}
{/* eslint-disable-next-line jsx-a11y/click-events-have-key-events */}
<span
style={{padding: '4px', cursor: 'pointer'}}
onClick={e => {
e.stopPropagation()
removeSelectedItem(null)
}}
>
✕
</span>
</span>
)
},
)}
<div>
<input
style={{padding: '4px'}}
{...getInputProps(getDropdownProps({preventKeyAction: isOpen}))}
data-testid="combobox-input"
/>
<button
style={{padding: '4px 8px'}}
aria-label="toggle menu"
data-testid="combobox-toggle-button"
{...getToggleButtonProps()}
>
{isOpen ? <>↑</> : <>↓</>}
</button>
<button
style={{padding: '4px 8px'}}
aria-label="clear selection"
data-testid="clear-button"
onClick={clearSelection}
>
✗
</button>
</div>
</div>
<ul
{...getMenuProps()}
style={{
listStyle: 'none',
width: '100%',
padding: '0',
margin: '4px 0 0 0',
}}
>
{isOpen &&
items.map((item, index) => (
<li
style={{
padding: '4px',
backgroundColor: highlightedIndex === index ? '#bde4ff' : null,
}}
key={`${item}${index}`}
{...getItemProps({
item,
index,
'data-testid': `downshift-item-${index}`,
})}
>
{item}
</li>
))}
</ul>
</div>
)
}
render(<DropdownMultipleCombobox />, document.getElementById('root'))And useTagGroup:
import * as React from 'react'
import {render} from 'react-dom'
import {useTagGroup, useCombobox} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
function TagGroup() {
const initialItems = colors.slice(0, 5)
const [inputValue, setInputValue] = React.useState('')
const {
addItem,
getTagProps,
getTagRemoveProps,
getTagGroupProps,
items,
activeIndex,
} = useTagGroup({initialItems})
const itemsToAdd = colors.filter(
color =>
!items.includes(color) &&
(!inputValue || color.toLowerCase().includes(inputValue.toLowerCase())),
)
const {
isOpen,
getToggleButtonProps,
getLabelProps,
getMenuProps,
getInputProps,
highlightedIndex,
getItemProps,
} = useCombobox({
items: itemsToAdd,
inputValue,
onInputValueChange: changes => {
setInputValue(changes.inputValue)
},
onSelectedItemChange(changes) {
if (changes.selectedItem) {
addItem(changes.selectedItem)
}
},
selectedItem: null,
stateReducer(_state, actionAndChanges) {
const {changes, type} = actionAndChanges
if (
changes.selectedItem &&
type !== useCombobox.stateChangeTypes.InputBlur
) {
return {...changes, inputValue: '', highlightedIndex: 0, isOpen: true}
}
return changes
},
})
return (
<div>
<div
{...getTagGroupProps({'aria-label': 'colors example'})}
className="tag-group"
>
{items.map((color, index) => (
<span
className={`${index === activeIndex ? 'selected-tag' : ''} tag`}
key={color}
{...getTagProps({index, 'aria-label': color})}
>
{color}
<button
className="tag-remove-button"
type="button"
{...getTagRemoveProps({index, 'aria-label': 'remove'})}
>
✕
</button>
</span>
))}
</div>
<div className="wrapper">
<label {...getLabelProps()}>Choose your favorite book:</label>
<div className="input-wrapper">
<input
placeholder="Best book ever"
className="text-input"
{...getInputProps()}
/>
<button
aria-label="toggle menu"
className="toggle-button"
type="button"
{...getToggleButtonProps()}
>
{isOpen ? <>↑</> : <>↓</>}
</button>
</div>
</div>
<ul className="menu" {...getMenuProps()}>
{isOpen
? itemsToAdd.map((item, index) => (
<li
className={`menu-item${index === highlightedIndex ? ' highlighted' : ''}`}
key={item}
{...getItemProps({item, index})}
>
<span>{item}</span>
</li>
))
: null}
</ul>
</div>
)
}
render(<TagGroup />, document.getElementById('root'))Selection state
For starters:
- both hooks manage state internally
- both hooks accept controlled props
- both hooks return an imperative function to add items to selection
The difference is that one could easly rely on the default internal state ofuseTagGroup in order to build the multiple seleciton combobox. Consequently,
there's no need for:
const [selectedItems, setSelectedItems] = React.useState(initialSelectedItems)Also, since we rely on useTagGroup to manage state on its own, there's no need
to hook an onStageChange prop in order to capture the removal state changes
and update selectedItems as a result. Consequently, just remove these props:
selectedItems,
onStateChange({selectedItems: newSelectedItems, type}) {
switch (type) {
case useMultipleSelection.stateChangeTypes
.SelectedItemKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete:
case useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace:
case useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem:
setSelectedItems(newSelectedItems)
break
default:
break
}
},We also need to make sure that adding items to the selection works, so we still
need to use addItem when we detect a combobox selection. Since we don't have a
controlled state anymore, the onStateChange from useCombobox turns from:
onStateChange({
inputValue: newInputValue,
type,
selectedItem: newSelectedItem,
}) {
switch (type) {
case useCombobox.stateChangeTypes.InputKeyDownEnter:
case useCombobox.stateChangeTypes.ItemClick:
setSelectedItems([...selectedItems, newSelectedItem])
break
case useCombobox.stateChangeTypes.InputChange:
setInputValue(newInputValue)
break
default:
break
}
},To:
onSelectedItemChange(changes) {
if (changes.selectedItem) {
addItem(changes.selectedItem)
}
},Notice that in the useMultipleSelection example we use a different
implementation to filter the examples, hence the setInputValue onuseCombobox.stateChangeTypes.InputChange. Both strategies achieve the same
purpose, but the useTagGroup example has a cleaner code. Anyway, choose
whatever you prefer, or any other way to filter the combobox items from the
already selected and the input value.
Notice we keep controlling the selectedItem for useCombobox since we are
controlling the actual selection, and useCombobox only supports single
selection by default.
JSX
Our tag group is now accessible, and this involves some getter prop function
changes.
getTagGroupProps
This is new, just use it on the tags container. Looking at theuseMultipleSelectionCode that will be the `` after the <label>.
getTagProps
This is equivalent to getSelectedItemProps.
getDropdownProps
This has no equivalent in useTagGroup, and it involved a coupling between theuseMultipleSelection and useCombobox hooks. No need for it now, just usegetInputProps without anything extra.
getTagRemoveProps
In the useMultipleSelection example, notice we have an explicit onClick prop
that removed the selected item imperatively. With useTagGroup we will do it
declaratively by using the getTagRemoveProps. Basically, your X icon button
will turn from this:
<span
style={{padding: '4px', cursor: 'pointer'}}
onClick={e => {
e.stopPropagation()
removeSelectedItem(null)
}}
>To this
<button
style={{padding: '4px', cursor: 'pointer'}}
type="button"
{...getTagRemoveProps({index, 'aria-label': 'remove'})}
>Using a button element might be better, but keep in mind that by default we
are removing it from the tab order, since keyboard users can just use
Delete/Backspace when a selected item is focused.
Wrapping up
We hope that useTagGroup provides a way better experience for your users,
since we aimed to make it more accessible, either when using it by itself or
building tag based multiple selection for selects and comboboxes. Feel free to
provide more tips for usage or migrations by opeing a PR / issue in Github in
order to help other users. Thank you!
File: src/hooks/useSelect/README.md
useSelect
The problem
You have a custom select dropdown in your application and you want it to behave
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 dropdown select. You also want this solution to be
simple to use and flexible so you can tailor it further to your specific needs.
This solution
useSelect is a React hook that manages all the stateful logic needed to make
the dropdown 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. The props are similar to the ones
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.
Migration through breaking changes
The hook received breaking changes related to how it works, as well as the API,
starting with v7. They are documented here:
Table of Contents
- Usage
- Basic Props
- Advanced Props
- isItemDisabled
- initialSelectedItem
- initialIsOpen
- initialHighlightedIndex
- defaultSelectedItem
- defaultIsOpen
- defaultHighlightedIndex
- itemToKey
- getA11yStatusMessage
- onHighlightedIndexChange
- onIsOpenChange
- onStateChange
- highlightedIndex
- isOpen
- selectedItem
- id
- labelId
- menuId
- toggleButtonId
- getItemId
- environment
- stateChangeTypes
- Control Props
- Returned props
- Event Handlers
- Examples
Usage
import * as React from 'react'
import {render} from 'react-dom'
import {useSelect} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
function DropdownSelect() {
const {
isOpen,
selectedItem,
getToggleButtonProps,
getLabelProps,
getMenuProps,
highlightedIndex,
getItemProps,
} = useSelect({items: colors})
return (
<div
style={{
display: 'flex',
flexDirection: 'column',
width: 'fit-content',
justifyContent: 'center',
marginTop: 100,
alignSelf: 'center',
}}
>
<label
style={{
fontWeight: 'bolder',
color: selectedItem ? selectedItem : 'black',
}}
{...getLabelProps()}
>
Choose an element:
</label>
<div
style={{
padding: '4px',
textAlign: 'center',
border: '1px solid black',
backgroundColor: 'lightgray',
cursor: 'pointer',
}}
{...getToggleButtonProps()}
>
{selectedItem ?? 'Elements'}
{isOpen ? <>↑</> : <>↓</>}
</div>
<ul
{...getMenuProps()}
style={{
listStyle: 'none',
width: '100%',
padding: '0',
margin: '4px 0 0 0',
}}
>
{isOpen &&
colors.map((item, index) => (
<li
style={{
padding: '4px',
backgroundColor: highlightedIndex === index ? '#bde4ff' : null,
}}
key={`${item}${index}`}
{...getItemProps({
item,
index,
})}
>
{item}
</li>
))}
</ul>
</div>
)
}
render(<DropdownSelect />, document.getElementById('root'))Basic Props
This is the list of props that you should probably know about. There are some
advanced props below as well.
items
any[]| required
The main difference from vanilla Downshift is that we pass the items we want
to render to the hook as well. Opening the menu with an item already selected
means the hook has to know in advance what items you plan to render and what is
the position of that item in the list. Consequently, there won't be any need for
two state changes: one for opening the menu and one for setting the highlighted
index, like in Downshift.
itemToString
function(item: any)| defaults to:item => (item ? String(item) : '')
If your items are stored as, say, objects instead of strings, downshift still
needs a string representation for each one. This is required for accessibility
aria-live messages (e.g., after making a selection) and for keyboard interaction
features (e.g., highlighting an item by typing its first few characters).
onSelectedItemChange
function(changes: object)| optional, no useful default
Called each time the selected item was changed. Selection can be performed by
item click, Enter or Space Key while item is highlighted or by using character
keys while the toggle button is focused and menu is closed.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theselectedItemproperty
with the newly selected value. This also has atypeproperty which you can
learn more about in thestateChangeTypessection. This
property will be part of the actions that can trigger aselectedItemchange,
for exampleuseSelect.stateChangeTypes.ItemClick.
stateReducer
function(state: object, actionAndChanges: object)| optional
🚨 This is a really handy power feature 🚨
This function will be called each time useSelect sets its internal state (or
calls your onStateChange handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates. It gives you the current state
and the state that will be set, and you return the state that you want to set.
state: The full current state of downshift.actionAndChanges: Object that contains the actiontype, thepropspassed
touseSelect, thechangessuggested by the Downshift default reducer, and
any action-specific fields (e.g.altKey,key,index). About thetype
property you can learn more about in thestateChangeTypessection.
import {useSelect} from 'downshift'
import {items} from './utils'
const {getMenuProps, getItemProps, ...rest} = useSelect({
items,
stateReducer,
})
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
// this prevents the menu from being closed when the user selects an item with 'Enter' or mouse
switch (type) {
case useSelect.stateChangeTypes.ToggleButtonKeyDownEnter:
case useSelect.stateChangeTypes.ItemClick:
return {
...changes, // default Downshift new state changes on item selection.
isOpen: state.isOpen, // but keep menu open.
highlightedIndex: state.highlightedIndex, // with the item highlighted.
}
default:
return changes // otherwise business as usual.
}
}NOTE: This is only called when state actually changes. You should not attempt
use this to handle events. If you wish to handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For exampleshould be). Also, your
reducer function should be "pure." This means it should do nothing other than
return the state changes you want to have happen.
Advanced Props
isItemDisabled
function(item: any, index: number)| defaults to:(_item, _index) => false
If an item needs to be marked as disabled, this function needs to return true
for that item. Disabled items will be skipped from keyboard navigation, will not
be selected and will be marked as disabled for screen readers.
initialSelectedItem
any| defaults tonull
Pass an item that should be selected when downshift is initialized.
initialIsOpen
boolean| defaults tofalse
Pass a boolean that sets the open state of the menu when downshift is
initialized.
initialHighlightedIndex
number| defaults to-1
Pass a number that sets the index of the highlighted item when downshift is
initialized.
defaultSelectedItem
any| defaults tonull
Pass an item that should be selected when downshift is reset.
defaultIsOpen
boolean| defaults tofalse
Pass a boolean that sets the open state of the menu when downshift is reset or
when an item is selected.
defaultHighlightedIndex
number| defaults to-1
Pass a number that sets the index of the highlighted item when downshift is
reset or when an item is selected.
itemToKey
function(item: any)| defaults to:item => item
Used to determine the uniqueness of an item when searching for the item or
comparing the item with another. Returns the item itself, by default, so the
comparing/searching is done internally via referential equality.
If using items as objects and their reference will change during use, you can
use the function to generate a unique key for each item, such as an id prop.
// initial items.
const items = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
// the same items but with different references, for any reason.
const newItems = [
{id: 1, value: 'Apples'},
{id: 2, value: 'Oranges'},
]
function itemToKey(item) {
return item.id
// and we will do the comparison like: const isChanged = itemToKey(prevSelectedItem) !== itemToKey(nextSelectedItem)
}getA11yStatusMessage
function({/* see below */})| default messages provided in English
This function is passed as props to a status updating function nested within
that allows you to create your own ARIA statuses. It is called when the state
changes: selectedItem, highlightedIndex, inputValue or isOpen.
There is no default function provided anymore since v9, so if there's no prop
passed, no aria live status message is created. An implementation that resembles
the previous default is written below, should you want to keep pre v9 behaviour.
We don't provide this as a default anymore since we consider that screen readers
have been significantly improved and they can convey information about items
count, possible actions and highlighted items only from the HTML markup, without
the need for aria-live regions.
function getA11yStatusMessage(state) {
if (!state.isOpen) {
return ''
}
// you need to get resultCount and previousResultCount yourself now, since we don't pass them as arguments anymore
const resultCount = items.length
const previousResultCount = previousResultCountRef.current
if (!resultCount) {
return 'No results are available.'
}
if (resultCount !== previousResultCount) {
return `${resultCount} result${
resultCount === 1 ? ' is' : 's are'
} available, use up and down arrow keys to navigate. Press Enter or Space Bar keys to select.`
}
return ''
}onHighlightedIndexChange
function(changes: object)| optional, no useful default
Called each time the highlighted item was changed. Items can be highlighted
while hovering the mouse over them or by keyboard keys such as Up Arrow, Down
Arrow, Home and End. Items can also be highlighted by hitting character keys
that are part of their starting string equivalent.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain thehighlightedIndex
property with the new value. This also has atypeproperty which you can
learn more about in thestateChangeTypessection. This
property will be part of the actions that can trigger ahighlightedIndex
change, for exampleuseSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp.
onIsOpenChange
function(changes: object)| optional, no useful default
Called each time the menu is open or closed. Menu can be open by toggle button
click, Enter, Space, Up Arrow or Down Arrow keys. Can be closed by selecting an
item, blur (Tab, Shift-Tab or clicking outside), clicking the toggle button
again or hitting Escape key.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theisOpenproperty with
the new value. This also has atypeproperty which you can learn more about
in thestateChangeTypessection. This property will be
part of the actions that can trigger aisOpenchange, for exampleuseSelect.stateChangeTypes.ToggleButtonClick.
onStateChange
function(changes: object)| optional, no useful default
This function is called anytime the internal state changes. This can be useful
if you're using downshift as a "controlled" component, where you manage some or
all of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then
pass it as props, rather than letting downshift control all its state itself.
changes: These are the properties that actually have changed since the last
state change. This also has atypeproperty which you can learn more about
in thestateChangeTypessection.
Tip: This function will be called any time any state is changed. The best
way to determine whether any particular state was changed, you can usechanges.hasOwnProperty('propName')or use theon[statePropKey]Changeprops
described above.
NOTE: This is only called when state actually changes. You should not attempt
to use this to handle events. If you wish handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For example:should be).
highlightedIndex
number| control prop (read more about this in
the Control Props section)
The index of the item that should be highlighted when menu is open.
isOpen
boolean| control prop (read more about this in
the Control Props section)
The open state of the menu.
selectedItem
any| control prop (read more about this in
the Control Props section)
The item that should be selected.
id
string| defaults to a generated ID
Used to generate the first part of the Downshift id on the elements. You can
override this id with one of your own, provided as a prop, or you can override
the id for each element altogether using the props below.
labelId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (label) you usegetLabelProps with.
menuId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (ul) you usegetMenuProps with.
toggleButtonId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element (button) you usegetToggleButtonProps with.
getItemId
function(index)| defaults to a function that generates an ID based on the
index
Used for aria attributes and the id prop of the element (li) you usegetItemProps with.
environment
window| defaults towindow
This prop is only useful if you're rendering downshift within a differentwindow context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/oradd|removeEventListener on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
access to these properties
for downshift.
stateChangeTypes
There are a few props that expose changes to state
(onStateChange and stateReducer). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a type property on the changes
object you get. This type corresponds to a stateChangeTypes property.
The list of all possible values this type property can take is defined in
this file and is as follows:
useSelect.stateChangeTypes.ToggleButtonKeyDownArrowDownuseSelect.stateChangeTypes.ToggleButtonKeyDownArrowUpuseSelect.stateChangeTypes.ToggleButtonKeyDownEscapeuseSelect.stateChangeTypes.ToggleButtonKeyDownHomeuseSelect.stateChangeTypes.ToggleButtonKeyDownEnduseSelect.stateChangeTypes.ToggleButtonKeyDownPageUpuseSelect.stateChangeTypes.ToggleButtonKeyDownPageDownuseSelect.stateChangeTypes.ToggleButtonKeyDownEnteruseSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButtonuseSelect.stateChangeTypes.ToggleButtonKeyDownCharacteruseSelect.stateChangeTypes.ToggleButtonBluruseSelect.stateChangeTypes.ToggleButtonClickuseSelect.stateChangeTypes.MenuMouseLeaveuseSelect.stateChangeTypes.ItemMouseMoveuseSelect.stateChangeTypes.ItemClickuseSelect.stateChangeTypes.FunctionToggleMenuuseSelect.stateChangeTypes.FunctionOpenMenuuseSelect.stateChangeTypes.FunctionCloseMenuuseSelect.stateChangeTypes.FunctionSetHighlightedIndexuseSelect.stateChangeTypes.FunctionSelectItemuseSelect.stateChangeTypes.FunctionSetInputValueuseSelect.stateChangeTypes.FunctionReset
See stateReducer for a concrete example on how to use thetype property.
Control Props
Downshift manages its own state internally and calls youronSelectedItemChange, onIsOpenChange, onHighlightedIndexChange,onInputChange and onStateChange handlers with any relevant changes. The
state that downshift manages includes: isOpen, selectedItem, inputValue
and highlightedIndex. Returned action function (read more below) can be used
to manipulate this state and can likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon asthis.props[statePropKey] !== undefined, internally, downshift will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where onStateChange
comes in really handy), but you can also control the state from anywhere, be
that state from other components, redux, react-router, or anywhere else.
Note: This is very similar to how normal controlled components work elsewhere
in react (like ``). If you want to learn more about this concept, you
can learn about that from the Advanced React Component Patterns
course
Returned props
You use the hook like so:
import {useSelect} from 'downshift'
import {items} from './utils'
const {getToggleButtonProps, reset, ...rest} = useSelect({
items,
...otherProps,
})
return (
<div>
<div {...getToggleButtonProps()}>Options</div>
{/* render the menu and items */}
{/* render a button that resets the select to defaults */}
<button
onClick={() => {
reset()
}}
>
Reset
</button>
</div>
)NOTE: In this example we used both a getter prop
getToggleButtonPropsand an
action propreset. The properties ofuseSelectcan be split into three
categories as indicated below:
prop getters
NOTE: These prop-getters provide
aria-attributes which are very important
to your component being accessible. It's recommended that you utilize these
functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question, for example on the toggle button:
`` element. |
| getItemProps | function({}) | returns the props you should apply to any menu item elements you render. |
| getLabelProps | function({}) | returns the props you should apply to the label element that you render. |
| getMenuProps | function({}) | returns the props you should apply to the ul element (or root of your menu) that you render. |
`getLabelProps`
This method should be applied to the label you render. It will generate anid that will be used to label the toggle button and the menu.
There are no required properties for this method.
Note: For accessibility purposes, calling this method is highly recommended.
`getMenuProps`
This method should be applied to the element which contains your list of items.
Typically, this will be a or a that surrounds a map expression.
This handles the proper ARIA roles and attributes.
Optional properties:
ref: if you need to access the menu element via a ref object, you'd call the
function like this:getMenuProps({ref: yourMenuRef}). As a result, the menu
element will receive a composedrefproperty, which guarantees that both
your code anduseSelectuse the same correct reference to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getMenuProps({refKey: 'innerRef'})and
your composite component would forward like:. However, if you are just rendering a primitive component like, there
is no need to specify this property. It defaults toref.Please keep in mind that menus, for accessiblity purposes, should always be
rendered, regardless of whether you hide it or not. Otherwise,getMenuProps
may throw error if you unmount and remount the menu.aria-label: By default the menu will add anaria-labelledbythat refers to
the<label>rendered withgetLabelProps. However, if you providearia-labelto give a more specific label that describes the options
available, thenaria-labelledbywill not be provided and screen readers can
use youraria-labelinstead.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetMenuProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useSelect will unexpectedly
fail.
const {getMenuProps} = useSelect({items})
const ui = (
<ul {...getMenuProps()}>
{!isOpen
? null
: items.map((item, index) => (
<li {...getItemProps({item, index, key: item.id})}>{item.name}</li>
))}
</ul>
)Note that for accessibility reasons it's best if you always render this
element whether or not downshift is in anisOpenstate.
`getItemProps`
The props returned from calling this function should be applied to any menu
items you render.
This is an impure function, so it should only be called when you will
actually be applying the props to an item.
What do you mean by impure function?
Basically just don't do this:
items.map((item, index) => {
const props = getItemProps({item, index}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return <div {...props} />
})Instead, you could do this:
items.filter(shouldRenderItem).map(item => <div {...getItemProps({item})} />)Required properties:
The main difference from vanilla Downshift is that we require the items as
props before rendering. The reason is to open the menu with items already
highlighted, and we need to know the items before the actual render. It is still
required to pass either item or index to getItemProps.
item: this is the item data that will be selected when the user selects a
particular item.index: This is howdownshiftkeeps track of your item when updating thehighlightedIndexas the user keys around. By default,downshiftwill
assume theindexis the order in which you're callinggetItemProps. This
is often good enough, but if you find odd behavior, try setting this
explicitly. It's probably best to be explicit aboutindexwhen using a
windowing library likereact-virtualized.
Optional properties:
ref: if you need to access the item element via a ref object, you'd call the
function like this:getItemProps({ref: yourItemRef}). As a result, the item
element will receive a composedrefproperty, which guarantees that both
your code anduseSelectuse the same correct reference to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getItemProps({refKey: 'innerRef'})and
your composite component would forward like:<li ref={props.innerRef} />.
However, if you are just rendering a primitive component like ``, there
is no need to specify this property. It defaults toref.
`getToggleButtonProps`
Call this and apply the returned props to a div element that will act as a
triggering button. Since ARIA 1.2, the trigger element will reiceve a combobox
role, so a `` is not considered correct markup anymore. It allows you to
toggle the Menu component.
Optional properties:
disabled: If this is set totrue, then all of the downshift button event
handlers will be omitted (it won't toggle the menu when clicked).ref: if you need to access the button element via a ref object, you'd call
the function like this:getToggleButtonProps({ref: yourToggleButtonRef}). As
a result, the button element will receive a composedrefproperty, which
guarantees that both your code anduseSelectuse the same correct reference
to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getToggleButton({refKey: 'innerRef'})
and your composite component would forward like:. However, if you are just rendering a primitive component like, there is no need to specify this property.
It defaults toref.aria-label: By default the toggle element will add anaria-labelledbythat
refers to the<label>rendered withgetLabelProps. However, if you providearia-labelto give a more specific label that describes the options
available, thenaria-labelledbywill not be provided and screen readers can
use youraria-labelinstead.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetToggleButtonProps. Please use it with extreme care and only if you are
absolutely sure that the ref is correctly forwarded otherwise useSelect will
unexpectedly fail.
const {getToggleButtonProps} = useSelect({items})
const myButton = (
<button {...getToggleButtonProps()}>Click me</button>
{/* menu and items */}
)actions
These are functions you can call to change the state of the downshiftuseSelect hook.
| property | type | description |
|---|---|---|
closeMenu |
function() |
closes the menu |
openMenu |
function() |
opens the menu |
selectItem |
function(item: any) |
selects the given item |
setHighlightedIndex |
function(index: number) |
call to set a new highlighted index |
toggleMenu |
function() |
toggle the menu open state |
reset |
function() |
this resets downshift's state to a reasonable default |
state
These are values that represent the current state of the downshift component.
| property | type | description |
|---|---|---|
highlightedIndex |
number |
the currently highlighted item |
isOpen |
boolean |
the menu open state |
selectedItem |
any |
the currently selected item input |
keysSoFar |
string |
the character keys typed so far |
Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call event.preventDefault(). Their additional functionality is
described below.
Default handlers
Toggle Button
Click: If the menu is not displayed, it will open it. Otherwise it will
close it. If there is already an item selected, the menu will be opened with
that item already highlighted.Enter: If the menu is closed, opens the menu. If the menu is opened and
there is an item highlighted, it will select that item.Space: If the menu is closed, opens the menu. If the menu is opened and
there is an item highlighted, it will select that item. If the user has typed
character keys before pressingSpace, the space character will concatenate
to the search query. This allows search for options such asRepublic of ...CharacterKey: Opens the menu if closed and highlights the first option that
starts with that key. For instance, typingCwill select the option that
starts withC. PressingCagain will move the highlight to the next item
that starts withC. Typing keys into rapid succession (in less than 500ms
each) will select the option starting with that key combination, for instance
typingCALwill selectcaliforniumif this option exists.ArrowDown: If the menu is closed, it will open it. If there is already an
item selected, it will open the menu with the selected item highlighted.
Otherwise, it will open the menu with the first option highlighted. If the
menu is already open, it will highlight the next item.ArrowUp: If the menu is closed, it will open it. If there is already an item
selected, it will open the menu with the selected item highlighted. Otherwise,
it will open the menu with the last option highlighted. If the menu is already
open, it will highlight the previous item.Alt+ArrowDown: If the menu is closed, it will open it, without highlighting
any item.Alt+ArrowUp: If the menu is open, it will close it and will select the item
that was highlighted.End: If the menu is closed, it will open it. It will also highlight the last
item in the list.Home: If the menu is closed, it will open it. It will also highlight the
first item in the list.PageUp: If the menu is open, it will move the highlight the item 10
positions before the current selection.PageDown: If the menu is open, it will move the highlight the item 10
positions after the current selection.Escape: It will close the menu without selecting anything and keeps focus to
the toggle button.Blur(Tab, Shift+Tab, MouseClick outside): It will close the menu will select
the highlighted item if any. Focus is handled naturally (next / previous
elemenent in the tab order, body element if click outside.).
Menu
MouseLeave: Will clear the value of thehighlightedIndexif it was set.
Item
Click: It will select the item and close the menu.MouseOver: It will highlight the item.
Label
Click: It will move focus to the toggle element.
Customizing Handlers
You can provide your own event handlers to useSelect which will be called
before the default handlers:
const items = [...] // items here.
const {getMenuProps} = useSelect({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps({
onKeyDown: event => {
// your custom keyDown handler here.
},
})}
/>
)If you would like to prevent the default handler behavior in some cases, you can
set the event's preventDownshiftDefault property to true:
const {getMenuProps} = useSelect({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps({
onKeyDown: event => {
// your custom keyDown handler here.
if (event.key === 'Enter') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
})}
/>
)If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
const items = [...] // items here.
const {getMenuProps} = useSelect({items})
const ui = (
/* button, label, ... */
<ul
{...getMenuProps()}
onKeyDown={event => {
// your custom keyDown handler here.
}}
/>
)Examples
Usage examples are kept on the downshift docsite and also on the
sandbox repo. Each example has a link to its own Codesandbox
version, so check the docs.
It can be a great contributing opportunity to provide relevant use cases as
docsite examples. If you have such an example, please create an issue with the
suggestion and the Codesandbox for it, and we will take it from there.
File: src/hooks/useTagGroup/README.md
useTagGroup
The problem
You want to build a tag group component in your app that's accessible and offers
a great user experience. There is no dedicated ARIA design pattern for this
component, but since it's widely used, we compiled the list of specifications
and implemented them through a React hook that's compliant with Downshift's
principles.
This solution
useTagGroup is a React hook that manages all the stateful logic needed to make
the tag group functional and accessible. It returns a set of props that are
meant to be called and their results destructured on the tag group's elements:
its container, tag item and tag remove button. The props are similar to the ones
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 tag group to
implement the list of requirements. Every functionality needed should be
provided out-of-the-box: item removal and selection, and left/right arrow
movement between items, screen reader support etc.
Table of Contents
- Usage
- Basic Props
- Advanced Props
- stateChangeTypes
- Control Props
- Returned props
- Event Handlers
- Examples
Usage
import * as React from 'react'
import {render} from 'react-dom'
import {useTagGroup} from 'downshift'
const colors = [
'Black',
'Red',
'Green',
'Blue',
'Orange',
'Purple',
'Pink',
'Orchid',
'Aqua',
'Lime',
'Gray',
'Brown',
'Teal',
'Skyblue',
]
function TagGroup() {
const initialItems = colors.slice(0, 5)
const {
addItem,
getTagProps,
getTagRemoveProps,
getTagGroupProps,
items,
activeIndex,
} = useTagGroup({initialItems})
const itemsToAdd = colors.filter(color => !items.includes(color))
return (
<div>
<div
{...getTagGroupProps({'aria-label': 'colors example'})}
style={{
display: 'inline-flex',
gap: '8px',
alignItems: 'center',
flexWrap: 'wrap',
padding: '6px',
}}
>
{items.map((color, index) => (
<span
key={color}
{...getTagProps({index, 'aria-label': color})}
style={{
border: '1px solid darkgreen',
backgroundColor: 'green',
padding: '0 6px',
margin: '0 2px',
borderRadius: '10px',
cursor: 'default',
fontStyle: index === activeIndex ? 'italic' : 'normal',
}}
>
{color}
<button
type="button"
{...getTagRemoveProps({index, 'aria-label': 'remove'})}
style={{
padding: '4px',
cursor: 'pointer',
border: 'none',
backgroundColor: 'transparent',
}}
>
✕
</button>
</span>
))}
</div>
<div>Add more items:</div>
<ul>
{itemsToAdd.map(item => (
<li key={item}>
<button
tabIndex={0}
onClick={() => addItem(item)}
onKeyDown={({key}) => {
key === 'Enter' && addItem(item)
}}
style={{
cursor: 'pointer',
background: 'none',
border: 'none',
padding: 0,
}}
>
{item}
</button>
</li>
))}
</ul>
</div>
)
}
render(<TagGroup />, document.getElementById('root'))Basic Props
This is the list of props that you should probably know about. There are some
advanced props below as well.
removeElementDescription
string| defaults to:"Press Delete or Backspace to remove tag."
An accessible description that gets added to the DOM in an invisible element and
gets picked up by screen readers when encountering tags. It should instruct
users how to remove tags with the keyboard. Useful for localized messages.
onItemsChange
function(changes: object)| optional, no useful default
Called each time the items in state changed. Adding items can be done usingaddItem, while removing items could be done with mouse and keyboard actions.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theitemsproperty with
the newly selected value. This also has atypeproperty which you can learn
more about in thestateChangeTypessection. This
property will be part of the actions that can trigger aitemschange, for
exampleuseTagGroup.stateChangeTypes.FunctionAddItem.
stateReducer
function(state: object, actionAndChanges: object)| optional
🚨 This is a really handy power feature 🚨
This function will be called each time useTagGroup sets its internal state (or
calls your onStateChange handler for control props). It allows you to modify
the state change that will take place which can give you fine grain control over
how the component interacts with user updates. It gives you the current state
and the state that will be set, and you return the state that you want to set.
state: The full current state of useTagGroup.actionAndChanges: Object that contains the actiontype, props needed to
return a new state based on that type and the changes suggested by the
useTagGroup default reducer. About thetypeproperty you can learn more
about in thestateChangeTypessection.
import {useTagGroup} from 'downshift'
import {items} from './utils'
const {getTagGroupProps, getTagProps, getTagRemoveProps, ...rest} = useTagGroup(
{
initialItems: items.slice(0, 4),
stateReducer,
},
)
function stateReducer(state, actionAndChanges) {
const {type, changes} = actionAndChanges
// resets active item to the first when removing an item
switch (type) {
case useSelect.stateChangeTypes.TagGroupKeyDownBackspace:
case useSelect.stateChangeTypes.TagGroupKeyDownDelete:
return {
...changes, // default tagGroup new state changes on item removal.
activeIndex: changes.items.length === 0 ? -1 : 0,
}
default:
return changes // otherwise business as usual.
}
}NOTE: This is only called when state actually changes. You should not attempt
use this to handle events. If you wish to handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For exampleshould be). Also, your reducer
function should be "pure." This means it should do nothing other than return
the state changes you want to have happen.
Advanced Props
isItemDisabled
function(item: any, index: number)| defaults to:(_item, _index) => false
If an item needs to be marked as disabled, this function needs to return true
for that item. Disabled items will be skipped from keyboard navigation, will not
be removed and will be marked as disabled for screen readers.
initialItems
any[]| defaults tonull
Pass an items collection that should be selected when useTagGroup is
initialized.
initialActiveIndex
number| defaults to0if there are initial items or-1otherwise.
Pass a boolean that sets the open state of the menu when useTagGroup is
initialized.
onActiveIndexChange
function(changes: object)| optional, no useful default
Called each time the active index was changed. Active item can be changed by
clicking the tag or by using left and right arrows while a tag is focused.
changes: These are the properties that actually have changed since the last
state change. This object is guaranteed to contain theactiveIndexproperty
with the new value. This also has atypeproperty which you can learn more
about in thestateChangeTypessection. This property
will be part of the actions that can trigger aactiveIndexchange, for
exampleuseTagGroup.stateChangeTypes.TagGroupKeyDownArrowRight.
onStateChange
function(changes: object)| optional, no useful default
This function is called anytime the internal state changes. This can be useful
if you're using useTagGroup as a "controlled" hook, where you manage some or all
of the state (e.g., isOpen, selectedItem, highlightedIndex, etc) and then pass
it as props, rather than letting useTagGroup control all its state itself.
changes: These are the properties that actually have changed since the last
state change. This also has atypeproperty which you can learn more about
in thestateChangeTypessection.
Tip: This function will be called any time any state is changed. The best
way to determine whether any particular state was changed, you can usechanges.hasOwnProperty('propName')or use theon[statePropKey]Changeprops
described above.
NOTE: This is only called when state actually changes. You should not attempt
to use this to handle events. If you wish handle events, put your event
handlers directly on the elements (make sure to use the prop getters though!
For example:should be).
activeIndex
number| control prop (read more about this in
the Control Props section)
The index of the item that should be active so that it receives focus.
items
any[]| control prop (read more about this in
the Control Props section)
The items that are part of the tag group
id
string| defaults to a generated ID
Used to generate the first part of the useTagGroup id on the elements. You can
override this id with one of your own, provided as a prop, or you can override
the id for each element altogether using the props below.
tagGroupId
string| defaults to a generated ID
Used for aria attributes and the id prop of the element you usegetTagGroupProps with.
getTagId
function(index)| defaults to a function that generates an ID based on the
index
Used for aria attributes and the id prop of the element (li) you usegetTagProps and getTagRemoveProps
with.
environment
window| defaults towindow
This prop is only useful if you're rendering downshift within a differentwindow context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/oradd|removeEventListener on its prototype (as is the case for a shadow-root)
then you will need to pass in a custom object that is able to provide
access to these properties
for downshift.
stateChangeTypes
There are a few props that expose changes to state
(onStateChange and stateReducer). For you
to make the most of these APIs, it's important for you to understand why state
is being changed. To accomplish this, there's a type property on the changes
object you get. This type corresponds to a stateChangeTypes property.
The list of all possible values this type property can take is defined in
this file and is as follows:
useTagGroup.stateChangeTypes.TagClickuseTagGroup.stateChangeTypes.TagGroupKeyDownArrowLeftuseTagGroup.stateChangeTypes.TagGroupKeyDownArrowRightuseTagGroup.stateChangeTypes.TagGroupKeyDownBackspaceuseTagGroup.stateChangeTypes.TagGroupKeyDownDeleteuseTagGroup.stateChangeTypes.TagRemoveClickuseTagGroup.stateChangeTypes.FunctionAddItem
See stateReducer for a concrete example on how to use thetype property.
Control Props
Downshift manages its own state internally and calls your onItemsChange,onActiveIndexChange and onStateChange handlers with any relevant changes.
The state that downshift manages includes: items and activeIndex. Returned
action function (read more below) can be used to manipulate this state and can
likely support many of your use cases.
However, if more control is needed, you can pass any of these pieces of state as
a prop (as indicated above) and that state becomes controlled. As soon asthis.props[statePropKey] !== undefined, internally, downshift will determine
its state based on your prop's value rather than its own internal state. You
will be required to keep the state up to date (this is where onStateChange
comes in really handy), but you can also control the state from anywhere, be
that state from other components, redux, react-router, or anywhere else.
Note: This is very similar to how normal controlled components work elsewhere
in react (like ``). If you want to learn more about this concept, you
can learn about that from the Advanced React Component Patterns
course
Returned props
You use the hook like so:
import {useTagGroup} from 'downshift'
import {items} from './utils'
const {getTagGroupProps, addItem, ...rest} = useTagGroup({
initialItems: items.slice(0, 2),
...otherProps,
})
return (
<div>
<div {...getTagGroupProps()}>{/* render the items */}</div>
<div>
{/* render items to be added */}
<button
onClick={() => {
addItem(itemToBeAdded)
}}
>
{`Add ${itemToBeAdded.title}`}
</button>
</div>
</div>
)NOTE: In this example we used both a getter prop
getTagGroupPropsand an
action propaddItem. The properties ofuseTagGroupcan be split into three
categories as indicated below:
prop getters
NOTE: These prop-getters provide
aria-attributes which are very important
to your component being accessible. It's recommended that you utilize these
functions and apply the props they give you to your components.
These functions are used to apply props to the elements that you render. This
gives you maximum flexibility to render what, when, and wherever you like. You
call these on the element in question, for example on the toggle button:or a. This applies
ARIA attributes and event handlers.
Required properties:
index: This is howuseTagRemoveGroupkeeps track of your item when
changing the active element throughactiveIndex. By default,useTagRemoveGroupwill assume theindexis the order in which you're
callinggetTagProps. This is often good enough, but if you find odd
behavior, try setting this explicitly. It's probably best to be explicit aboutindexwhen using a windowing library likereact-virtualized.
Optional properties:
ref: if you need to access the menu element via a ref object, you'd call the
function like this:getTagProps({ref: yourTagRef}). As a result, the tag
element will receive a composedrefproperty, which guarantees that both
your code anduseTagGroupuse the same correct reference to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getTagProps({refKey: 'innerRef'})and
your composite component would forward like:. However, if you are just rendering a primitive component like, there
is no need to specify this property. It defaults toref.
In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument togetTagProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useTagGroup will
unexpectedly fail.
const {getTagProps, getTagGroupProps, items} = useTagGroup()
const ui = (
<div {...getTagGroupProps()}>
{!items.length === 0
? null
: items.map((item, index) => (
<span {...getTagProps({index, key: item.id})}>{item.name}</span>
))}
</div>
)This is an impure function, so it should only be called when you will
actually be applying the props to an item.
What do you mean by impure function?
Basically just don't do this:
items.map((item, index) => {
const props = getTagProps({item, index}) // we're calling it here
if (!shouldRenderItem(item)) {
return null // but we're not using props, and downshift thinks we are...
}
return <div {...props} />
})Instead, you could do this:
items.filter(shouldRenderItem).map(item => <div {...getTagProps({item})} />)`getTagRemoveProps`
The props returned from calling this function should be applied to the remove
button element for each tag, if there is need for such an element
This is an impure function, same as getTagProps.
// rendering the tag
<span {...getTagProps({index, key: item.id})}>
{item.name}
<button {...getTagRemoveProps({index, 'aria-label': 'Remove'})}>X</button>
</span>Required properties:
index: This is howuseTagRemoveGroupkeeps track of your item when
labelling the remove button. By default,useTagRemoveGroupwill assume theindexis the order in which you're callinggetItemProps. This is often
good enough, but if you find odd behavior, try setting this explicitly. It's
probably best to be explicit aboutindexwhen using a windowing library likereact-virtualized.
Optional properties:
ref: if you need to access the item element via a ref object, you'd call the
function like this:getTagRemoveProps({ref: yourTagRef}). As a result, the
tag element will receive a composedrefproperty, which guarantees that both
your code anduseSelectuse the same correct reference to the element.refKey: if you're rendering a composite component, that component will need
to accept a prop which it forwards to the root DOM element. Commonly, folks
call thisinnerRef. So you'd call:getTagRemoveProps({refKey: 'innerRef'})
and your composite component would forward like:. However, if you are just rendering a primitive component like, there is no need to specify this property.
It defaults toref.
actions
These are functions you can call to change the state of the downshiftuseTagGroup hook.
| property | type | description |
|---|---|---|
addItem |
function(item: any) |
it adds the given item to the collection |
state
These are values that represent the current state of the downshift component.
| property | type | description |
|---|---|---|
activeIndex |
number |
the currently active item |
items |
any[] |
the items in the selection |
Event Handlers
Downshift has a few events for which it provides implicit handlers. Several of
these handlers call event.preventDefault(). Their additional functionality is
described below.
Default handlers
Tag Group
ArrowLeft: if a tag is focused and there are multiple tags in the tag group,
it will move focus to the tag that's placed to the left. If the tag has no
neighbours to the left, it will move focus to the last tag.ArrowRight: if a tag is focused and there are multiple tags in the tag
group, it will move focus to the tag that's placed to the right. If the tag
has no neighbours to the right, it will move focus to the first tag.
Tag
Click: moves the focus to this tag. Useful to execute optional actions as
well, such as opening a non-modal with additional information.Delete, Backspace: removes the tag from the tag group and moves the focus to
either the next or previous tag, depending on the removed tag index.
Tag Remove
Click: removes the tag from the tag group and moves the focus to either the
next or previous tag, depending on the removed tag index.
Customizing Handlers
You can provide your own event handlers to useTagGroup which will be called
before the default handlers:
const items = [...] // items here.
const {getTagGroup} = useTagGroup({initialItems: items})
const ui = (
<div
{...getTagGroup({
onKeyDown: event => {
// your custom keyDown handler here.
},
})}
/>
)If you would like to prevent the default handler behavior in some cases, you can
set the event's preventDownshiftDefault property to true:
const items = [...] // items here.
const {getTagGroup} = useTagGroup({initialItems: items})
const ui = (
<div
{...getTagGroup({
onKeyDown: event => {
// your custom keyDown handler here.
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
// Prevent Downshift's default 'Enter' behavior.
event.nativeEvent.preventDownshiftDefault = true
// your handler code
}
},
})}
/>
)If you would like to completely override Downshift's behavior for a handler, in
favor of your own, you can bypass prop getters:
const items = [...] // items here.
const {getTagGroup} = useTagGroup({initialItems: items})
const ui = (
<div
{...getTagGroup()}
onKeyDown={event => {
// your custom keyDown handler here.
}}
/>
)Examples
Usage examples are kept on the downshift docsite and also on the
sandbox repo. Each example has a link to its own Codesandbox
version, so check the docs.
It can be a great contributing opportunity to provide relevant use cases as
docsite examples. If you have such an example, please create an issue with the
suggestion and the Codesandbox for it, and we will take it from there.
File: test/tsconfig.json
{
"extends": "../tsconfig.json",
"compilerOptions": {
"noEmit": true
},
"include": ["../typings/index.d.ts"]
}
File: .github/ISSUE_TEMPLATE.md
downshiftversion:nodeversion:npm(oryarn) version:
Relevant code or config
What you did:
What happened:
Reproduction repository:
Problem description:
Suggested solution:
File: .github/PULL_REQUEST_TEMPLATE.md
Pull Request
What
Why
How
Changes
Checklist
- Documentation
- Tests
- TypeScript Types
- Ready to be merged
File: .github/workflows/validate.yml
name: validate
on:
push:
branches:
- '+([0-9])?(.{+([0-9]),x}).x'
- 'master'
- 'next'
- 'next-major'
- 'beta'
- 'alpha'
- '!all-contributors/**'
pull_request: {}
jobs:
main:
# ignore all-contributors PRs
if: ${{ !contains(github.head_ref, 'all-contributors') }}
strategy:
matrix:
node: [20, 22, 24]
runs-on: ubuntu-latest
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v5
- name: Increase watchers
run:
echo fs.inotify.max_user_watches=524288 | sudo tee -a /etc/sysctl.conf
&& sudo sysctl -p
- name: ⎔ Setup node
uses: actions/setup-node@v5
with:
node-version: ${{ matrix.node }}
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
useLockFile: false
- name: 🎭 Install Playwright browsers
run: npx playwright install --with-deps chromium
- name: ▶️ Run validate script
run: npm run validate
env:
FORCE_COLOR: true
- name: ⬆️ Upload coverage report
uses: codecov/codecov-action@v5
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} release:
needs: main
runs-on: ubuntu-latest
if:
${{ github.repository == 'downshift-js/downshift' &&
contains('refs/heads/master,refs/heads/beta,refs/heads/next,refs/heads/alpha',
github.ref) && github.event_name == 'push' }}
steps:
- name: 🛑 Cancel Previous Runs
uses: styfle/[email protected]
with:
access_token: ${{ secrets.GITHUB_TOKEN }}
- name: ⬇️ Checkout repo
uses: actions/checkout@v5
- name: ⎔ Setup node
uses: actions/setup-node@v5
with:
node-version: 24
- name: 📥 Download deps
uses: bahmutov/npm-install@v1
with:
useLockFile: false
- name: 🏗 Run build script
run: npm run build
- name: 🚀 Release
uses: cycjimmy/semantic-release-action@v6
with:
semantic_version: 24
branches: |
[
'+([0-9])?(.{+([0-9]),x}).x',
'master',
'next',
'next-major',
{name: 'beta', prerelease: true},
{name: 'alpha', prerelease: true}
]
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}