Repository navigation
Tabs: a disabled selected tab makes the whole tablist keyboard-unreachable #10633
Description
Provide a general summary of the issue here
When the selected tab is also in disabledKeys (or disabled directly), the roving tabindex is left pointing at a tab that renders no tabIndex at all, and no other tab is reachable by keyboard.
🤔 Expected Behavior?
When the selected tab is disabled, the roving tabindex should fall back to an enabled tab, matching the existing arrow-key behavior that already skips disabled tabs.
😯 Current Behavior
useTabListState points focusedKey at selectedKey with no check against disabledKeys, and its reselect fallback only runs when the selection is empty or missing (#3162), never when it's merely disabled. useTab gives a disabled tab no tabIndex. Every enabled tab ends up at tabindex="-1", the disabled selected tab has none, and the tablist itself has none either. Tab/Shift+Tab skip the whole tablist and land on the tab panel (or the first focusable element inside it, if there is one); arrow keys can't help since they only move focus once it's already inside the list. I confirmed there is no keyboard route in at all: full Tab/Shift+Tab cycles plus all four arrows, Home, and End, from before the tabs, after the tabs, and from the panel, in both jsdom and headless Chrome. Only a mouse click on an enabled tab recovers.
Three ways I found into this state:
defaultSelectedKey/selectedKeynames a tab disabled from the start.- A selection that starts enabled and later becomes disabled (controlled or uncontrolled).
- Every tab disabled at mount, then some re-enabled: selection sticks to the first (disabled) key.
Related: #10278 adds an opt-in allowFocusWhenDisabled per-item prop. If merged, setting it on the selected tab would work around this, but the default (non-opt-in) behavior above is unchanged.
💁 Possible Solution
I haven't tested a fix, just read the source, so take this as a guess: useTabListState sets focusedKey from selectedKey unconditionally. Falling back to the nearest enabled key when the selected one is disabled, mirroring the existing skip-disabled arrow-key behavior, looks like it would close all three routes above, but I haven't verified it has no side effects elsewhere.
🔦 Context
Found this while auditing keyboard operability of a Tabs component built directly on react-aria-components. The selection reaching a disabled state isn't misuse. It happens through ordinary controlled-prop updates, not just an invalid initial key (which I understand is the separate, already-discussed case in #9584). Right now the only mitigation available at the consuming layer is a dev-time warning, which is diagnostic, not a fix. If there's anything else I can share, let me know!
🖥️ Steps to Reproduce
<Tabs selectedKey="appearance" disabledKeys={["appearance"]}>
<TabList aria-label="Settings">
<Tab id="general">General</Tab>
<Tab id="appearance">Appearance</Tab>
<Tab id="notifications">Notifications</Tab>
</TabList>
<TabPanel id="general">General content</TabPanel>
<TabPanel id="appearance">Appearance content</TabPanel>
<TabPanel id="notifications">Notifications content</TabPanel>
</Tabs>Tab into the page: focus skips the tablist entirely and lands on the panel. No arrow key, Tab, or Shift+Tab combination reaches any tab. Only clicking an enabled tab recovers.
Live reproduction: https://stackblitz.com/edit/dupebdjj?file=index.html
🌍 Your Environment
Version
react-aria-components 1.20.0 (react-aria 3.51.0, react-stately 3.49.0)
What browsers are you seeing the problem on?
Chrome
If other, please specify.
No response
What operating system are you using?
macOS
🦄 Other
🧢 Your Company/Team
No response
🕷 Tracking Issue
No response
Activity
snowystinger commentedon Sep 25, 2026
I think we'd say that you shouldn't disable the selected key. You also know best which key should be selected if the case that the current one becomes disabled. We'd just be making a guess. Do you have a real use case where this happens? or is it just theoretical?
andrewpucci commentedon Sep 28, 2026
Yes, it's a real use case for us, though our app avoids it today with a guard of its own. Looking into it also showed me the fix I suggested in the issue was wrong. I think the better fix is for a selected tab to stay focusable when it's disabled. Here's a generic version of the use case, then the reasoning.
Imagine a CI build page with Logs, Tests, and Artifacts tabs. Artifacts is disabled until the build finishes (disabled rather than hidden, so the tabs don't jump around while a build runs), and the selected tab lives in the URL so it can be shared. Two ordinary things leave the selected tab disabled:
You're right that the app knows best which tab to select, and that's how we handle it: we fall back to Logs when the requested tab is disabled. My concern is that every feature has to remember that guard. If one forgets, nothing looks wrong with a mouse, but keyboard users can't reach any tab in the list.
On the fix, I originally asked for the roving tabindex to fall back to an enabled tab. The APG Tabs pattern says focus entering the tab list goes to the active tab, so moving it to a different tab would break that.
Focusability of disabled controls fits better. It lists tabs among the elements its examples keep focusable with
aria-disabled="true", since screen reader users are far less likely to discover disabled elements they can't focus. Applied here, the selected tab keepstabindex="0"while disabled, so focus lands on it and the arrow keys move on from there. Nothing about selection or focus has to be guessed.#10278's
allowFocusWhenDisableddoes this as an opt-in, but its tests don't cover a tab that's both selected and disabled, so I tried that on its branch (jsdom only, test below). With the opt-in, pressing Tab lands on the selected disabled tab and the arrow keys move on, in both activation modes, including when the tab becomes disabled after mount or while it has focus. Without it, the tab list is still skipped.Test run against #10278's branch
Would it make sense for a selected tab to stay focusable when it's disabled by default, even without #10278? That only changes a state where the tab list is unreachable today, so I don't think anyone is relying on it, and other disabled tabs would still be skipped as they are now.
snowystinger commentedon Sep 29, 2026
If
artifactsshould be discoverable while waiting for the build to be finished, then it seems like it should not be disabled and should have a content area with a progress of some variety?The biggest problem with allowing it to be disabled but still focusable is that people usually style disabled things in a way that doesn't meet contrast (doesn't need to for WCAG compliance). This means the element is not discoverable to users with low sight but who aren't using a screen reader. To them it could just look like a focus ring on empty space.
We've not discussed #10278 yet as a team, so I cannot say if we will be taking it on or what will happen to it yet.
andrewpucci commentedon Sep 29, 2026
Yes, I agree with that. And I think that's probably the right way to go in that situation.
But the generic scenario I described above is not quite that scenario. It's not meant to be discoverable... the tab was valid and not disabled when the URL was copied and shared, but something in the state changed and when the URL is visited again, the tab is not meant to be discoverable. Now, it is more that it needs to be recoverable, it's not an ideal state.
snowystinger commentedon Sep 29, 2026
Then this applies:
When the app opens again from that URL you'd set the selectedKey to whatever it should be. You are controlling the selectedKey, we can't just set it to something else, you could elect to ignore that. If we forced it, then your state would be out of sync with reality.
There are a few different issues in here, I've been focused on one. So just to quickly clarify.
andrewpucci commentedon Oct 13, 2026
Thanks for breaking those out, that's a fair split. I agree on the controlled cases, and we already warn in dev on our side when
selectedKeyis indisabledKeys.For the uncontrolled cases, #10702 extends the existing fallback in
useTabListStateto cover case 2, and case 3 as well if you decide that one's a bug. I ran it against my repro cases in jsdom on React 18 and 19, and it holds up apart from one edge case I left a note about there.One thing I noticed: with that change, an uncontrolled
defaultSelectedKeythat names a disabled tab also falls back to the first enabled tab, so it touches case 1 as well.