Summary
In cmdk 1.1.1, Command.Input never exposes a usable aria-activedescendant: it is absent at a pristine mount and, once set, goes stale after the filter changes — so it can name a row other than the one aria-selected marks and other than the one Enter activates.
Environment
- cmdk
1.1.1, React 19, Chromium (headless, via Playwright), measured 2026-09-04.
- A plain
Command with an Input, a List, two Groups of Items ("Open jobs", "New job" / "Profile", "Sign out"), uncontrolled value, default filtering. No disablePointerSelection, no custom filter.
Steps to reproduce
- Mount the palette and read the input's attributes before touching it.
- Observed: the first item carries
aria-selected="true", and the input has no aria-activedescendant attribute at all.
- Type a query that leaves one match (
sign).
- Observed: still no
aria-activedescendant.
- Clear the query, press ArrowDown until "Profile" is selected, type
pro, then clear the query again.
- Observed:
aria-activedescendant now names "Profile" while aria-selected="true" is on "Open jobs" (the first item, re-selected on clear).
- Press Enter at that state.
- Observed:
cmdk-item-select dispatches on "Open jobs".
So after step 3 the row a screen reader is told about (aria-activedescendant → "Profile") is not the row that is selected or the row Enter runs ("Open jobs"). In two runs the attribute named an item id that was no longer in the DOM at all.
Expected
aria-activedescendant on the input tracks the selected item on every change — including the initial selection and re-selection after a filter — so it always names the same item aria-selected="true" is on and the one Enter would activate. Per the ARIA combobox/listbox pattern, the input's aria-activedescendant is what assistive technology uses to announce the current option; a stale or missing value means a screen-reader user hears the wrong row or none.
Why a consumer cannot work around it
Command.Input spreads incoming props before its own attributes and then writes aria-activedescendant from useCommandState(m => m.selectedItemId), so a value passed from outside is overwritten by the library. (The same ordering means role passed to Command.Separator is discarded — a separate, smaller issue.) Patching the package or re-implementing the input inside the cmdk root are the remaining options, neither of which a consumer should need.
Where the id seems to go stale
selectedItemId is read from the store, but the store's selected id is derived from value and the item registry; when the filter unmounts the selected item and the first visible item is re-selected, the id the input renders lags one update behind (or points at an unmounted node), and at first mount nothing sets it at all. I have not traced the exact line and may be wrong about the mechanism; the observations above are what we measured.
Context
Found while auditing keyboard and screen-reader behaviour of a design system that composes Select and Menu components on cmdk. Happy to test a fix or a canary build against the same driven sequence.
Summary
In cmdk 1.1.1,
Command.Inputnever exposes a usablearia-activedescendant: it is absent at a pristine mount and, once set, goes stale after the filter changes — so it can name a row other than the onearia-selectedmarks and other than the one Enter activates.Environment
1.1.1, React 19, Chromium (headless, via Playwright), measured 2026-09-04.Commandwith anInput, aList, twoGroups ofItems ("Open jobs", "New job" / "Profile", "Sign out"), uncontrolledvalue, default filtering. NodisablePointerSelection, no customfilter.Steps to reproduce
aria-selected="true", and the input has noaria-activedescendantattribute at all.sign).aria-activedescendant.pro, then clear the query again.aria-activedescendantnow names "Profile" whilearia-selected="true"is on "Open jobs" (the first item, re-selected on clear).cmdk-item-selectdispatches on "Open jobs".So after step 3 the row a screen reader is told about (
aria-activedescendant→ "Profile") is not the row that is selected or the row Enter runs ("Open jobs"). In two runs the attribute named an item id that was no longer in the DOM at all.Expected
aria-activedescendanton the input tracks the selected item on every change — including the initial selection and re-selection after a filter — so it always names the same itemaria-selected="true"is on and the one Enter would activate. Per the ARIA combobox/listbox pattern, the input'saria-activedescendantis what assistive technology uses to announce the current option; a stale or missing value means a screen-reader user hears the wrong row or none.Why a consumer cannot work around it
Command.Inputspreads incoming props before its own attributes and then writesaria-activedescendantfromuseCommandState(m => m.selectedItemId), so a value passed from outside is overwritten by the library. (The same ordering meansrolepassed toCommand.Separatoris discarded — a separate, smaller issue.) Patching the package or re-implementing the input inside the cmdk root are the remaining options, neither of which a consumer should need.Where the id seems to go stale
selectedItemIdis read from the store, but the store's selected id is derived fromvalueand the item registry; when the filter unmounts the selected item and the first visible item is re-selected, the id the input renders lags one update behind (or points at an unmounted node), and at first mount nothing sets it at all. I have not traced the exact line and may be wrong about the mechanism; the observations above are what we measured.Context
Found while auditing keyboard and screen-reader behaviour of a design system that composes
SelectandMenucomponents on cmdk. Happy to test a fix or a canary build against the same driven sequence.