mrkeyoor.com_
Sat 08 Aug 20:58 UTC
npmWeb Frontendupdated 08 Aug 2026

downshift

Downshift supplies headless React state and accessibility behavior for autocompletes, editable comboboxes, custom selects, and removable tag groups. Its hooks return state, actions, and prop-getter functions; you render the label, input or button, menu, options, tags, and styling, then spread each getter onto the correct element. useCombobox, useSelect, and useTagGroup follow current ARIA-oriented patterns. The older Downshift render-prop component remains exported but the README recommends hooks and says that component will eventually be removed.

Verdict

Downshift remains one of the best choices for teams that want to own every pixel and DOM node while starting from serious interaction logic. Choose a native control or a more structured component library when your team does not want to assemble and continuously accessibility-test the widget itself.

API stability3/5The hook concepts and prop-getter pattern are established, but each of versions 7, 8, and 9 has a dedicated hook migration guide. Version 9 removed selectedItemChanged and several accessibility message props, changed callback typings, and stopped providing default hook status messages. The root README also says the older Downshift render-prop component will be removed once hooks are mature.
Docs4/5downshift-js.com, the root README, and separate hook READMEs provide extensive runnable examples, ARIA rationale, every option and return value, state change types, handler merging, control props, and migration guides. The volume is also a navigation cost: the root README spends much of its space on the legacy component, hook facts are split across long files, and some older packaging notes do not match the current exports map.
Maintenance5/5Downshift 9.4.0 and the latest repository push both date to June 2026. Recent 9.x releases added useTagGroup, explicit CJS and ESM extensions, React Compiler compatibility, hook TypeScript conversions, and fixes for disabled items, IDs, refs, and action props. GitHub reports 57 open issues and pull requests in its combined counter for a mature project with 12,310 stars.
Ecosystem5/5The package recorded 4,112,187 downloads for the measured week, supports React from 16.12 onward, includes Preact and React Native paths in the project, and ships types plus dual module builds. Its headless approach fits design systems and arbitrary styling. The tradeoff is that positioning, animation, data loading, and visual components come from your own stack or additional packages.

Use it if

  • A React design system needs complete control over markup and styling while reusing tested keyboard and screen-reader behavior
  • You need an editable autocomplete through useCombobox, a button-driven custom select through useSelect, or removable chips through useTagGroup
  • Complex interaction rules need controlled state, action callbacks, or a pure stateReducer rather than a fixed component API
  • Items are objects, disabled options or changing object identities matter, and you can supply itemToString and itemToKey consistently
Skip it if

Setup reality

Install downshift plus React; React 16.12 or newer is the declared peer range. Version 9.4.0 ships ESM, CommonJS, React Native, and TypeScript declarations, and marks the package side-effect-free for bundlers. The install is easy, but the first correct component is not. You must keep the menu element mounted, spread getLabelProps, getInputProps or getToggleButtonProps, getMenuProps, and getItemProps onto the intended elements, pass every rendered item and its index, and supply a null-safe itemToString. Object items whose references change need itemToKey, also null-safe. Downshift manages interaction state, not filtering, remote requests, list positioning, collision detection, virtualization, styling, or result fetching. Async autocomplete needs your own stale-response or AbortController logic. Getter functions merge user handlers; pass handlers into the getter instead of spreading the getter and then overwriting onKeyDown or onBlur. Conditional rendering can trigger ref warnings and can remove elements screen readers expect; suppressRefError exists for exceptional layouts, not as a default fix. Version 9 removed selectedItemChanged in favor of itemToKey, removed getA11ySelectionMessage and getA11yRemovalMessage, and no longer creates default aria-live status text for hooks. The generated ARIA markup may be sufficient, but localized or product-specific announcements now require getA11yStatusMessage or your own live region. Controlled props require updating them in the corresponding callback or the UI appears frozen. Shadow roots and iframes need an environment object with the right document and event listener methods. Test mouse, touch, Escape, Home and End, arrow keys, typeahead, blur, disabled items, focus return, and at least the screen readers your users rely on.

Patterns

Wire an accessible editable comboboxbuild-basic-combobox

import {useCombobox} from 'downshift';

function FruitCombobox({items}) {
  const {isOpen, highlightedIndex, getLabelProps, getInputProps, getMenuProps, getItemProps} = useCombobox({items});
  return <>
    <label {...getLabelProps()}>Fruit</label>
    <input {...getInputProps()} />
    <ul {...getMenuProps()}>
      {isOpen && items.map((item, index) => (
        <li key={item} {...getItemProps({item, index})} data-highlighted={highlightedIndex === index}>
          {item}
        </li>
      ))}
    </ul>
  </>;
}

Keep the menu mounted and apply every getter to its intended element. Downshift supplies behavior and ARIA props, not CSS or positioning.

Filter suggestions as input changesfilter-combobox-items

const [shown, setShown] = useState(allItems);
const combobox = useCombobox({
  items: shown,
  onInputValueChange({inputValue}) {
    const query = inputValue.toLowerCase();
    setShown(allItems.filter(item => item.toLowerCase().includes(query)));
  },
});

Filtering is your responsibility. For remote searches, cancel stale requests or compare request IDs before replacing the current items.

Give object items stable identity and textuse-object-items

const combobox = useCombobox({
  items: users,
  itemToString: user => user?.name ?? '',
  itemToKey: user => user?.id,
  onSelectedItemChange: ({selectedItem}) => saveUserId(selectedItem?.id ?? null),
});

Both conversion callbacks receive null. itemToKey replaces the selectedItemChanged prop removed in version 9.

Control selection from application statecontrol-selected-item

const [selectedItem, setSelectedItem] = useState(null);
const combobox = useCombobox({
  items,
  selectedItem,
  onSelectedItemChange({selectedItem: next}) {
    setSelectedItem(next);
  },
});

Once selectedItem is controlled, update it in the callback. Passing a value without updating it makes user selection appear not to work.

Skip unavailable optionsdisable-options

const combobox = useCombobox({
  items: plans,
  itemToString: plan => plan?.name ?? '',
  isItemDisabled: plan => plan.soldOut,
});

// Still pass every item and its real index to getItemProps.

Disabled items are skipped by keyboard navigation and cannot be selected. Render a visible disabled treatment as well.

Change selection behavior with a state reducerkeep-menu-open

const combobox = useCombobox({
  items,
  stateReducer(state, {type, changes}) {
    if (type === useCombobox.stateChangeTypes.ItemClick ||
        type === useCombobox.stateChangeTypes.InputKeyDownEnter) {
      return {...changes, isOpen: true, highlightedIndex: state.highlightedIndex};
    }
    return changes;
  },
});

Keep reducers pure. Version 9 passes action details plus a nested changes object; older examples may use a different shape.

Build a button-driven selectbuild-custom-select

const {isOpen, selectedItem, highlightedIndex, getLabelProps, getToggleButtonProps, getMenuProps, getItemProps} = useSelect({items});

return <>
  <label {...getLabelProps()}>Plan</label>
  <button type='button' {...getToggleButtonProps()}>{selectedItem ?? 'Choose a plan'}</button>
  <ul {...getMenuProps()}>
    {isOpen && items.map((item, index) => (
      <li key={item} {...getItemProps({item, index})} data-highlighted={highlightedIndex === index}>{item}</li>
    ))}
  </ul>
</>;

Use useSelect for a non-editable choice widget. If a native select satisfies the design, it remains the simpler accessibility baseline.

Create removable tags with roving focusbuild-tag-group

const {items, activeIndex, getTagGroupProps, getTagProps, getTagRemoveProps} = useTagGroup({
  initialItems: ['React', 'TypeScript'],
});

return <div {...getTagGroupProps({'aria-label': 'Skills'})}>
  {items.map((item, index) => (
    <span key={item} {...getTagProps({index, 'aria-label': item})}>
      {item}
      <button type='button' {...getTagRemoveProps({index, 'aria-label': `Remove ${item}`})}>x</button>
    </span>
  ))}
</div>;

useTagGroup was added in the 9.x line. Its getters implement tag focus and removal behavior; visual focus must still be styled clearly.

Add an event handler through a prop gettermerge-custom-handler

<input
  {...getInputProps({
    onBlur(event) {
      analytics.track('combobox-blur');
    },
  })}
/>

Pass handlers into the getter so Downshift can merge them. A later onBlur prop would overwrite the handler returned by getInputProps.

Provide version 9 aria-live status textannounce-custom-status

const combobox = useCombobox({
  items,
  getA11yStatusMessage({isOpen, highlightedIndex}) {
    if (!isOpen) return '';
    if (highlightedIndex >= 0) return `${items[highlightedIndex].name} highlighted`;
    return `${items.length} results available`;
  },
});

Version 9 hooks have no default status-message function and now pass only hook state. Compute counts and highlighted items from your own items array.

Clear a selection with a separate buttonclear-combobox-selection

const {selectItem, setInputValue} = useCombobox({items});

<button type='button' onClick={() => {
  selectItem(null);
  setInputValue('');
}}>
  Clear
</button>

Give the button an accessible name in icon-only UIs and decide explicitly where focus should remain after clearing.

Use the correct window environmentrender-in-iframe

const frameWindow = iframeRef.current?.contentWindow;
const combobox = useCombobox({
  items,
  environment: frameWindow ?? window,
});

Iframes and shadow roots need document and add/removeEventListener access from the rendered context. Wait until the target environment exists before showing the widget.

Alternatives

PackageRegistryPick it when
react-aria-componentsnpmYou want accessible React primitives with a more structured component API across many widget types
@headlessui/reactnpmYou use React or Tailwind and prefer headless components with more prescribed markup and transitions
@radix-ui/react-selectnpmYou specifically need a composable custom select with popper positioning and portal pieces included
cmdknpmThe UI is a command palette or searchable command menu rather than a general form combobox