Skip to content

Input's aria-activedescendant is absent at mount and goes stale after filtering, so it can name a row other than the selected one #413

Description

@aseutin

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

  1. 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.
  2. Type a query that leaves one match (sign).
    • Observed: still no aria-activedescendant.
  3. 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).
  4. 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.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions