downshift Documentation

A set of primitives to build simple, flexible, WAI-ARIA compliant React autocomplete, combobox or select dropdown components.

RAW Doc

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

  1. Fork and clone the repo
  2. npm run setup to setup and validate your clone of the project
  3. Create a branch for your PR

Tip: Keep your master branch pointing at the original repository and make
pull requests from branches on your fork. To do this, run:

text
git remote add upstream https://github.com/downshift-js/downshift.git
git fetch upstream
git branch --set-upstream-to=upstream/master master

This will add the original repository as a "remote" called "upstream," Then
fetch the git information from that remote, then set your local master
branch to use the upstream master branch whenever you run git pull. Then you
can make all of your pull request branches based on this master branch.
Whenever you want to update your version of master, do a regular git pull.

Committing and Pushing changes

Please make sure to run the tests before you commit your changes. You can run
npm 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 downshift is what we
    expect. These tests live in other/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 in src/__tests__
  • test:ts - This runs tsc on the codebase to make sure the type script
    definitions are correct for the tsx files in the test directory.
  • 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 in
    other/ssr/__tests__
  • test:e2e - This runs tests in an actual browser. It runs and tests the
    docusaurus examples. These tests live in e2e/.

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:

text
pre-commit

Help 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 called
semantic-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

text
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

text
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

text
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, useMultipleSelection to improve them.
  • remove the Downshift component 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.

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).
  • 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:

js
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:

js
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 a
useSelect.stateChangeTypes.ToggleButtonClick type 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:

js
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.

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:

js
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:

js
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 the
disabled prop in getItemProps.

Example of API migration:

Old:

jsx
const items = [{value: 'item1'}, {value: 'item2'}]

const {getInputProps, ...rest} = useCombobox({items})

return (
  // ... rest
  <li {...getItemProps({item, disabled: item.value === 'item2'})}>
)

New:

jsx
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.InputFocus has been removed.
  • useCombobox.stateChangeTypes.InputClick has 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:

js
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:

js
<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
  • 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

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.
  • Downshift is not affected, it has the same getA11yStatusMessage as 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:

itemToKey docs


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:
  • 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

Try it out in the browser

jsx
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 ? <>&#8593;</> : <>&#8595;</>}
        </button>
        <button
          style={{padding: '4px 8px'}}
          aria-label="toggle menu"
          data-testid="clear-button"
          onClick={() => selectItem(null)}
        >
          &#10007;
        </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 the selectedItem property
    with the newly selected value. This also has a type property which you can
    learn more about in the stateChangeTypes section. This
    property will be part of the actions that can trigger a selectedItem change,
    for example useCombobox.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 action type, the props passed
    to useCombobox, the changes suggested by the Downshift default reducer,
    and any action-specific fields (e.g. altKey, key, index). About the
    type property you can learn more about in the
    stateChangeTypes section.
javascript
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 example should 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 to null

Pass an item that should be selected when downshift is initialized.

initialIsOpen

boolean | defaults to false

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 to null

Pass an item that should be selected when downshift is reset.

defaultIsOpen

boolean | defaults to false

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.

js
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:

js
// 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.

js
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 the highlightedIndex
    property with the new value. This also has a type property which you can
    learn more about in the stateChangeTypes section. This
    property will be part of the actions that can trigger a highlightedIndex
    change, for example useCombobox.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 the isOpen property with
    the new value. This also has a type property which you can learn more about
    in the stateChangeTypes section. This property will be
    part of the actions that can trigger a isOpen change, for example
    useCombobox.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 the inputValue property
    with the new value. This also has a type property which you can learn more
    about in the stateChangeTypes section. This property
    will be part of the actions that can trigger a inputValue change, for
    example useCombobox.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 a type property which you can learn more about
    in the stateChangeTypes section.

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 use
changes.hasOwnProperty('propName') or use the on[statePropKey]Change props
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.

jsx
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 use
getLabelProps with.

string | defaults to a generated ID

Used for aria attributes and the id prop of the element (ul) you use
getMenuProps with.

toggleButtonId

string | defaults to a generated ID

Used for aria attributes and the id prop of the element (button) you use
getToggleButtonProps with.

inputId

string | defaults to a generated ID

Used for aria attributes and the id prop of the element (input) you use
getInputProps 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 use
getItemProps with.

environment

window | defaults to window

This prop is only useful if you're rendering downshift within a different
window context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/or
add|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.InputKeyDownArrowDown
  • useCombobox.stateChangeTypes.InputKeyDownArrowUp
  • useCombobox.stateChangeTypes.InputKeyDownEscape
  • useCombobox.stateChangeTypes.InputKeyDownHome
  • useCombobox.stateChangeTypes.InputKeyDownEnd
  • useCombobox.stateChangeTypes.InputKeyDownPageUp
  • useCombobox.stateChangeTypes.InputKeyDownPadeDown
  • useCombobox.stateChangeTypes.InputKeyDownEnter
  • useCombobox.stateChangeTypes.InputChange
  • useCombobox.stateChangeTypes.InputClick
  • useCombobox.stateChangeTypes.InputBlur
  • useCombobox.stateChangeTypes.MenuMouseLeave
  • useCombobox.stateChangeTypes.ItemMouseMove
  • useCombobox.stateChangeTypes.ItemClick
  • useCombobox.stateChangeTypes.ToggleButtonClick
  • useCombobox.stateChangeTypes.FunctionToggleMenu
  • useCombobox.stateChangeTypes.FunctionOpenMenu
  • useCombobox.stateChangeTypes.FunctionCloseMenu
  • useCombobox.stateChangeTypes.FunctionSetHighlightedIndex
  • useCombobox.stateChangeTypes.FunctionSelectItem
  • useCombobox.stateChangeTypes.FunctionSetInputValue
  • useCombobox.stateChangeTypes.FunctionReset

See stateReducer for a concrete example on how to use the
type property.

Control Props

Downshift manages its own state internally and calls your
onSelectedItemChange, 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 as
this.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:

javascript
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 getInputProps and an action
prop reset. The properties of useCombobox can be split into three
categories as indicated below:

prop getters

See the blog post about 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 this innerRef. 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 to ref.

    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 an aria-labelledby that refers to
    the <label> rendered with getLabelProps. However, if you provide
    aria-label to give a more specific label that describes the options
    available, then aria-labelledby will not be provided and screen readers can
    use your aria-label instead.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getMenuProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useCombobox will
unexpectedly fail.

jsx
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 an isOpen state.

`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:

jsx
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:

jsx
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 how downshift keeps track of your item when updating the
    highlightedIndex as the user keys around. By default, downshift will
    assume the index is the order in which you're calling getItemProps. This
    is often good enough, but if you find odd behavior, try setting this
    explicitly. It's probably best to be explicit about index when using a
    windowing library like react-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 composed ref property, which guarantees that both
    your code and useCombobox use 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 this innerRef. 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 to ref.

`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 composed ref property, which guarantees
    that both your code and useCombobox use 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 this innerRef. 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 to ref.

  • disabled: If this is set to true, then all of the downshift button event
    handlers will be omitted (it won't toggle the menu when clicked).

jsx
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
    from getInputProps and a disabled prop 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 composed ref property, which guarantees that
    both your code and useCombobox use 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 this innerRef. 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 to ref.

  • aria-label: By default the input will add an aria-labelledby that refers
    to the <label> rendered with getLabelProps. However, if you provide
    aria-label to give a more specific label that describes the options
    available, then aria-labelledby will not be provided and screen readers can
    use your aria-label instead.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getInput. 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 downshift
useCombobox 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 as Click. Button not in the tab order by
    default.
  • Space: Has the same effect as Click. Button not in the tab order by
    default.

Input

  • ArrowDown: Moves highlightedIndex one position down. When reaching the
    last option, ArrowDown will move highlightedIndex to first position.
  • ArrowUp: Moves highlightedIndex one position up. When reaching the first
    option, ArrowUp will move highlightedIndex to 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 the inputValue according to the value visible in
    the ``. Backspace or Space trigger 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 the input will become an empty string and the item
    stored as selectedItem will become null.
  • 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.
  • MouseLeave: Will clear the value of the highlightedIndex if it was set.

Item

  • Click: It will select the item, close the menu and move focus to the toggle
    button (unless defaultIsOpen is 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:

javascript
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:

javascript
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:

javascript
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 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
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

Try it out in the browser

javascript
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)
                  }}
                >
                  &#10005;
                </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 ? <>&#8593;</> : <>&#8595;</>}
          </button>
          <button
            style={{padding: '4px 8px'}}
            aria-label="clear selection"
            data-testid="clear-button"
            onClick={clearSelection}
          >
            &#10007;
          </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 the selectedItems
    property with the new array value. This also has a type property which you
    can learn more about in the stateChangeTypes section.
    This property will be part of the actions that can trigger an selectedItems
    change, for example useSelect.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 action type, props needed to
    return a new state based on that type and the changes suggested by the
    Downshift default reducer. About the type property you can learn more about
    in the stateChangeTypes section.
javascript
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 example should 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 to ArrowRight

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 to ArrowLeft

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.

js
// 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.

js
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 change
activeIndex 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 the activeIndex property
    with the new value. This also has a type property which you can learn more
    about in the stateChangeTypes section. This property
    will be part of the actions that can trigger a activeIndex change, for
    example useSelect.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 a type property which you can learn more about
    in the stateChangeTypes section.

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 use
changes.hasOwnProperty('propName') or use the on[statePropKey]Change props
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 to window

This prop is only useful if you're rendering downshift within a different
window context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/or
add|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.SelectedItemClick
  • useMultipleSelection.stateChangeTypes.SelectedItemKeyDownDelete
  • useMultipleSelection.stateChangeTypes.SelectedItemKeyDownBackspace
  • useMultipleSelection.stateChangeTypes.SelectedItemKeyDownNavigationNext
  • useMultipleSelection.stateChangeTypes.SelectedItemKeyDownNavigationPrevious
  • useMultipleSelection.stateChangeTypes.DropdownKeyDownNavigationPrevious
  • useMultipleSelection.stateChangeTypes.DropdownKeyDownBackspace
  • useMultipleSelection.stateChangeTypes.DropdownClick
  • useMultipleSelection.stateChangeTypes.FunctionAddSelectedItem
  • useMultipleSelection.stateChangeTypes.FunctionRemoveSelectedItem
  • useMultipleSelection.stateChangeTypes.FunctionSetSelectedItems
  • useMultipleSelection.stateChangeTypes.FunctionSetActiveIndex
  • useMultipleSelection.stateChangeTypes.FunctionReset

See stateReducer for a concrete example on how to use the
type property.

Control Props

Downshift manages its own state internally and calls your
onSelectedItemsChange, 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 as
this.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:

javascript
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 getSelectedItemProps and
getDropdownProps and an action prop reset. The properties of
useMultipleSelection can be split into three categories as indicated below:

prop getters

See the blog post about 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:

javascript
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:

jsx
selectedItems
  .filter(shouldRenderItem)
  .map(selectedItem => <div {...getSelectedItemProps({selectedItem})} />)

Required properties:

It is required to pass either selectedItem or index to
getSelectedItemProps 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 how downshift keeps track of your item when updating the
    activeIndex as the user keys around. By default, downshift will assume the
    index is the order in which you're calling getSelectedItemProps. This is
    often good enough, but if you find odd behavior, try setting this explicitly.
    It's probably best to be explicit about index when using a windowing library
    like react-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 composed ref property, which guarantees
    that both your code and useMultipleSelection use 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 this innerRef. 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 to ref.

`getDropdownProps`

Call this and apply the returned props to a button if you are building a
select 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: tells useMultipleSelection if dropdown is allowed to
    execute downshift handlers on keydown. For example, you can pass isOpen
    as value and user will not be able to delete selecteditems by Backspace or
    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 is false by 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 this innerRef. 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 to ref.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getDropdownProps. Please use it with extreme care and only if you are
absolutely sure that the ref is correctly forwarded otherwise
useMultipleSelection will unexpectedly fail.

javascript
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 downshift
useMultipleSelection 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

  • ArrowLeft: Moves focus from button/input to the last selected item and
    makes activeIndex to be selectedItems.length - 1. Performs this action if
    there are any items selected. ArrowLeft can be overriden with any other key
    depeding on the requirements. More info on
    keyNavigationPrevious.
  • Backspace: Removes the last selected item from selection. It always performs
    this action on a non-input element. If the dropdown is a combobox the text
    cursor of the input must be at the start of the input and not highlight
    any text in order for the removal to work.

Item

  • Click: It will make the item active, will modify activeIndex to reflect
    the new change, and will add focus to that item.
  • Delete: It will remove the item from selection. activeIndex will 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, the activeIndex will
    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 and
    activeIndex becomes -1.
  • Backspace: Same effect as Delete.
  • ArrowLeft: Moves activeIndex and focus to previous item. It stops at the
    first item in the selection. ArrowLeft can be overriden with any other key
    depeding on the requirements. More info on
    keyNavigationPrevious.
  • ArrowRight: Moves activeIndex and focus to next item. It will move focus
    to the dropdown if it occurs on the last selected item. ArrowRight can be
    overriden with any other key depeding on the requirements. More info on
    keyNavigationNext.

Customizing Handlers

You can provide your own event handlers to useMultipleSelection which will be
called before the default handlers:

javascript
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:

javascript
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:

javascript
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:

javascript
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)
                  }}
                >
                  &#10005;
                </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 ? <>&#8593;</> : <>&#8595;</>}
          </button>
          <button
            style={{padding: '4px 8px'}}
            aria-label="clear selection"
            data-testid="clear-button"
            onClick={clearSelection}
          >
            &#10007;
          </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:

jsx
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'})}
            >
              &#10005;
            </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 ? <>&#8593;</> : <>&#8595;</>}
          </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 of
useTagGroup in order to build the multiple seleciton combobox. Consequently,
there's no need for:

js
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:

js
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:

js
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:

js
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 on
useCombobox.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 the
useMultipleSelectionCode 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 the
useMultipleSelection and useCombobox hooks. No need for it now, just use
getInputProps 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:

javascript
<span
    style={{padding: '4px', cursor: 'pointer'}}
    onClick={e => {
      e.stopPropagation()
      removeSelectedItem(null)
    }}
  >

To this

javascript
<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

Try it out in the browser

jsx
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 ? <>&#8593;</> : <>&#8595;</>}
      </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 the selectedItem property
    with the newly selected value. This also has a type property which you can
    learn more about in the stateChangeTypes section. This
    property will be part of the actions that can trigger a selectedItem change,
    for example useSelect.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 action type, the props passed
    to useSelect, the changes suggested by the Downshift default reducer, and
    any action-specific fields (e.g. altKey, key, index). About the type
    property you can learn more about in the
    stateChangeTypes section.
javascript
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 example should 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 to null

Pass an item that should be selected when downshift is initialized.

initialIsOpen

boolean | defaults to false

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 to null

Pass an item that should be selected when downshift is reset.

defaultIsOpen

boolean | defaults to false

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.

js
// 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.

js
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 the highlightedIndex
    property with the new value. This also has a type property which you can
    learn more about in the stateChangeTypes section. This
    property will be part of the actions that can trigger a highlightedIndex
    change, for example useSelect.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 the isOpen property with
    the new value. This also has a type property which you can learn more about
    in the stateChangeTypes section. This property will be
    part of the actions that can trigger a isOpen change, for example
    useSelect.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 a type property which you can learn more about
    in the stateChangeTypes section.

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 use
changes.hasOwnProperty('propName') or use the on[statePropKey]Change props
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 use
getLabelProps with.

string | defaults to a generated ID

Used for aria attributes and the id prop of the element (ul) you use
getMenuProps with.

toggleButtonId

string | defaults to a generated ID

Used for aria attributes and the id prop of the element (button) you use
getToggleButtonProps 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 use
getItemProps with.

environment

window | defaults to window

This prop is only useful if you're rendering downshift within a different
window context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/or
add|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.ToggleButtonKeyDownArrowDown
  • useSelect.stateChangeTypes.ToggleButtonKeyDownArrowUp
  • useSelect.stateChangeTypes.ToggleButtonKeyDownEscape
  • useSelect.stateChangeTypes.ToggleButtonKeyDownHome
  • useSelect.stateChangeTypes.ToggleButtonKeyDownEnd
  • useSelect.stateChangeTypes.ToggleButtonKeyDownPageUp
  • useSelect.stateChangeTypes.ToggleButtonKeyDownPageDown
  • useSelect.stateChangeTypes.ToggleButtonKeyDownEnter
  • useSelect.stateChangeTypes.ToggleButtonKeyDownSpaceButton
  • useSelect.stateChangeTypes.ToggleButtonKeyDownCharacter
  • useSelect.stateChangeTypes.ToggleButtonBlur
  • useSelect.stateChangeTypes.ToggleButtonClick
  • useSelect.stateChangeTypes.MenuMouseLeave
  • useSelect.stateChangeTypes.ItemMouseMove
  • useSelect.stateChangeTypes.ItemClick
  • useSelect.stateChangeTypes.FunctionToggleMenu
  • useSelect.stateChangeTypes.FunctionOpenMenu
  • useSelect.stateChangeTypes.FunctionCloseMenu
  • useSelect.stateChangeTypes.FunctionSetHighlightedIndex
  • useSelect.stateChangeTypes.FunctionSelectItem
  • useSelect.stateChangeTypes.FunctionSetInputValue
  • useSelect.stateChangeTypes.FunctionReset

See stateReducer for a concrete example on how to use the
type property.

Control Props

Downshift manages its own state internally and calls your
onSelectedItemChange, 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 as
this.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:

javascript
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 getToggleButtonProps and an
action prop reset. The properties of useSelect can be split into three
categories as indicated below:

prop getters

See the blog post about 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 an
id 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 composed ref property, which guarantees that both
    your code and useSelect use 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 this innerRef. 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 to ref.

    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 an aria-labelledby that refers to
    the <label> rendered with getLabelProps. However, if you provide
    aria-label to give a more specific label that describes the options
    available, then aria-labelledby will not be provided and screen readers can
    use your aria-label instead.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getMenuProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useSelect will unexpectedly
fail.

jsx
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 an isOpen state.

`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:

jsx
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:

jsx
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 how downshift keeps track of your item when updating the
    highlightedIndex as the user keys around. By default, downshift will
    assume the index is the order in which you're calling getItemProps. This
    is often good enough, but if you find odd behavior, try setting this
    explicitly. It's probably best to be explicit about index when using a
    windowing library like react-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 composed ref property, which guarantees that both
    your code and useSelect use 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 this innerRef. 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 to ref.

`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 to true, 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 composed ref property, which
    guarantees that both your code and useSelect use 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 this innerRef. 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 to ref.

  • aria-label: By default the toggle element will add an aria-labelledby that
    refers to the <label> rendered with getLabelProps. However, if you provide
    aria-label to give a more specific label that describes the options
    available, then aria-labelledby will not be provided and screen readers can
    use your aria-label instead.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getToggleButtonProps. Please use it with extreme care and only if you are
absolutely sure that the ref is correctly forwarded otherwise useSelect will
unexpectedly fail.

jsx
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 downshift
useSelect 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 pressing Space, the space character will concatenate
    to the search query. This allows search for options such as Republic of ...
  • CharacterKey: Opens the menu if closed and highlights the first option that
    starts with that key. For instance, typing C will select the option that
    starts with C. Pressing C again will move the highlight to the next item
    that starts with C. Typing keys into rapid succession (in less than 500ms
    each) will select the option starting with that key combination, for instance
    typing CAL will select californium if 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.).
  • MouseLeave: Will clear the value of the highlightedIndex if 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:

javascript
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:

javascript
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:

javascript
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

Try it out in the browser

jsx
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',
              }}
            >
              &#10005;
            </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 using
addItem, 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 the items property with
    the newly selected value. This also has a type property which you can learn
    more about in the stateChangeTypes section. This
    property will be part of the actions that can trigger a items change, for
    example useTagGroup.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 action type, props needed to
    return a new state based on that type and the changes suggested by the
    useTagGroup default reducer. About the type property you can learn more
    about in the stateChangeTypes section.
javascript
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 example should 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 to null

Pass an items collection that should be selected when useTagGroup is
initialized.

initialActiveIndex

number | defaults to 0 if there are initial items or -1 otherwise.

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 the activeIndex property
    with the new value. This also has a type property which you can learn more
    about in the stateChangeTypes section. This property
    will be part of the actions that can trigger a activeIndex change, for
    example useTagGroup.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 a type property which you can learn more about
    in the stateChangeTypes section.

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 use
changes.hasOwnProperty('propName') or use the on[statePropKey]Change props
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 use
getTagGroupProps 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 use
getTagProps and getTagRemoveProps
with.

environment

window | defaults to window

This prop is only useful if you're rendering downshift within a different
window context from where your JavaScript is running; for example, an iframe
or a shadow-root. If the given context is lacking document and/or
add|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.TagClick
  • useTagGroup.stateChangeTypes.TagGroupKeyDownArrowLeft
  • useTagGroup.stateChangeTypes.TagGroupKeyDownArrowRight
  • useTagGroup.stateChangeTypes.TagGroupKeyDownBackspace
  • useTagGroup.stateChangeTypes.TagGroupKeyDownDelete
  • useTagGroup.stateChangeTypes.TagRemoveClick
  • useTagGroup.stateChangeTypes.FunctionAddItem

See stateReducer for a concrete example on how to use the
type 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 as
this.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:

javascript
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 getTagGroupProps and an
action prop addItem. The properties of useTagGroup can be split into three
categories as indicated below:

prop getters

See the blog post about 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 how useTagRemoveGroup keeps track of your item when
    changing the active element through activeIndex. By default,
    useTagRemoveGroup will assume the index is the order in which you're
    calling getTagProps. This is often good enough, but if you find odd
    behavior, try setting this explicitly. It's probably best to be explicit about
    index when using a windowing library like react-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 composed ref property, which guarantees that both
    your code and useTagGroup use 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 this innerRef. 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 to ref.

In some cases, you might want to completely bypass the refKey check. Then you
can provide the object {suppressRefError : true} as the second argument to
getTagProps. Please use it with extreme care and only if you are absolutely
sure that the ref is correctly forwarded otherwise useTagGroup will
unexpectedly fail.

jsx
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:

jsx
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:

jsx
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.

jsx
// 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 how useTagRemoveGroup keeps track of your item when
    labelling the remove button. By default, useTagRemoveGroup will assume the
    index is the order in which you're calling getItemProps. This is often
    good enough, but if you find odd behavior, try setting this explicitly. It's
    probably best to be explicit about index when using a windowing library like
    react-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 composed ref property, which guarantees that both
    your code and useSelect use 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 this innerRef. 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 to ref.

actions

These are functions you can call to change the state of the downshift
useTagGroup 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:

javascript
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:

javascript
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:

javascript
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

  • downshift version:
  • node version:
  • npm (or yarn) version:

Relevant code or config

javascript

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

text
- 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 }}

text
- 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 }}