downshift

Accessible React hooks and a render-prop primitive for building fully custom autocomplete, combobox, select, and multi-select components.

Library
npm
v9.4.0
12,304 stars
MIT License

Repository Health

Pre-computed score based on development activity, maintenance, community, maturity, and trend momentum. How we score it →
56 /100 Fair
Development Activity 12
Maintenance 44
Community 68
Maturity 60
Momentum 40

Technical Analysis

AI-assessed by reading the actual repository — architecture, code quality, innovation, and documentation. How we score it →
85 /100 Excellent
Architecture 78
Code Quality 88
Innovation 82
Learning Curve 90

Downshift provides the stateful logic behind autocomplete, combobox, select, and tag-group UI without dictating markup or styling. Rather than shipping pre-styled components, it exposes React hooks — useSelect, useCombobox, useTagGroup, and the legacy useMultipleSelection — plus the original Downshift render-prop component, each of which manages state transitions, keyboard navigation, and prop wiring so consuming code can render its own DOM structure.

The library was built to satisfy the WAI-ARIA authoring practices for combobox and listbox patterns, including the ARIA 1.2 combobox specification the hooks were migrated to in v7. It ships with TypeScript typings, Preact and React Native support, and a Docusaurus-based docs site with live examples, making it a common foundation for teams building fully custom dropdown-style inputs instead of depending on a pre-styled UI kit.

What You Get

  • useSelect, useCombobox, and useTagGroup hooks that manage open/closed state, highlighted index, keyboard navigation, and ARIA attribute wiring for custom dropdown components
  • The original Downshift render-prop component for teams not yet on the hook-based API, sharing the same accessible interaction model
  • Prop getters (getInputProps, getItemProps, getMenuProps, getToggleButtonProps, etc.) that attach the right event handlers and ARIA attributes without prescribing any markup
  • Full TypeScript typings, Preact interop, and a React Native build target alongside the standard web build
  • A stateReducer / reducer escape hatch for intercepting and overriding internal state transitions before they’re applied

Common Use Cases

  • Building a fully custom-styled autocomplete search input that still passes WAI-ARIA combobox audits
  • Implementing a multi-select or tag input where selected items render as removable chips
  • Replacing a native <select> with a custom-styled dropdown that needs keyboard and screen-reader parity with the native element
  • Powering design-system dropdown/combobox primitives that other teams compose into product-specific components

Under The Hood

Architecture

The core Downshift component (src/downshift.js) implements a class-based state machine handling isOpen/highlightedIndex/inputValue/selectedItem, routed through a stateReducer hook and exposed via prop-getters (getInputProps, getItemProps, getMenuProps, getToggleButtonProps, getRootProps) that attach ARIA attributes and event handlers computed from internal state. The hooks package (src/hooks) reimplements the same primitives on top of React’s useReducer via a shared reducer.ts and useEnhancedReducer/useControlledReducer utilities in src/hooks/utils, with per-hook modules (useSelect, useCombobox, useTagGroup, legacy useMultipleSelection) each supplying their own reducer cases, prop getters, and stateChangeTypes, while cross-cutting concerns (control-prop validation via useControlPropsValidator, scroll-into-view via useScrollIntoView, id generation via useElementIds) live as composable hooks in src/hooks/utils. It’s a layered “core reducer + prop-getter” pattern replicated per widget rather than a single monolith, so changes to the shared reducer/useControlledReducer layer ripple into all four hooks, while adding a new widget mainly means composing the existing utils into a fresh hook.

Tech Stack

Runtime dependencies are minimal — @babel/runtime, compute-scroll-into-view, prop-types, react-is, tslib — with react as the sole peer dependency. Build tooling is kcd-scripts driving Babel and Rollup to emit CJS, ESM, and React Native bundles alongside TypeScript declaration-only output for typings/index.d.ts. Docs are built with Docusaurus and deployed via Netlify. Testing runs on Jest for unit/integration coverage plus Playwright for browser-level e2e specs, and CI is a single GitHub Actions workflow (validate.yml) that runs lint, build-and-test, coverage, tsc —noEmit, an SSR check, and the e2e suite on every push.

Code Quality

The repo carries extensive test coverage — more than seventy test files under src/**/tests exercising nearly every prop-getter and utility function individually, plus top-level integration tests and Playwright e2e specs driving real browser interaction. tsconfig.json enables strict, strictNullChecks, and noUncheckedIndexedAccess, and the codebase mixes TypeScript with a smaller amount of legacy untyped JavaScript (mainly around the older useMultipleSelection hook). Linting and formatting are centralized via kcd-scripts and enforced through a husky pre-commit hook plus lint-staged, and CI gates every push behind the full validate script.

API Design Downshift’s defining design choice is a fully “headless” API: every interactive element’s props (onClick, onKeyDown, id, role, aria-*) are computed by calling a getter function and spreading the result, so consumers own all markup and styling while still getting ARIA-1.2-compliant combobox/listbox behavior. This predates and directly inspired later headless-UI libraries, and the stateReducer/control-props duality gives two escalation paths — override a single state transition inline, or hand state ownership entirely to an external store — without needing two different APIs. There is a genuine ramp-up cost to internalizing the prop-getter convention, which the project’s own FAQ and migration guides acknowledge directly.

Join founders buildingwith open source

Opinionated takes, migration guides, cost-saving tips, and insights from the open source ecosystem.

Subscribe on Substack
Join 750+ subscribers