[v3] Tabs component ARIA violations (aria-valid-attr-value, nested-interactive)
Description
HeroUI v3.0.0-beta.3 Tabs component has ARIA attribute violations detected by axe-core that block WCAG 2.1 Level A compliance.
Violations
1. Invalid aria-controls Reference (aria-valid-attr-value)
Severity: Critical (WCAG 2.1 Level A - 4.1.2 Name, Role, Value)
The Tabs component generates aria-controls attributes that reference non-existent IDs:
<div data-slot="tabs-tab"
role="tab"
aria-controls="react-aria-_r_1_-tabpanel-availability"
aria-selected="true">
Availability
</div>Issue: The ID react-aria-_r_1_-tabpanel-availability does not exist in the DOM.
axe-core error:
ARIA attributes must conform to valid values (aria-valid-attr-value)
Invalid ARIA attribute value: aria-controls="react-aria-_r_1_-tabpanel-availability"2. Nested Interactive Controls (nested-interactive)
Severity: Moderate (keyboard navigation confusion)
Popover triggers inside tab panels have focusable descendants, creating nested interactive elements:
<div class="popover__trigger"
role="button"
aria-expanded="false"
data-react-aria-pressable="true"
tabindex="0">
<button ...> <!-- Nested button inside button-like element -->axe-core error:
Interactive controls must not be nested (nested-interactive)
Fix any of the following:
Element has focusable descendantsImpact
- Screen Readers: ARIA controls mismatch causes incorrect tab panel announcements
- Keyboard Navigation: Nested interactive controls create focus traps
- WCAG Compliance: Fails WCAG 2.1 Level A (4.1.2 Name, Role, Value)
- Production Blocker: Cannot deploy accessibility-compliant applications with these violations
Reproduction
import { Tabs, Tab } from '@heroui/react';
<Tabs defaultSelectedKey="availability" variant="bordered">
<Tab key="availability" title="Availability">
Content 1
</Tab>
<Tab key="attempts" title="Attempts">
Content 2
</Tab>
</Tabs>Run axe-core accessibility tests:
import { axe } from 'jest-axe';
it('should have no accessibility violations', async () => {
const { container } = render(<MyTabsComponent />);
const results = await axe(container);
expect(results).toHaveNoViolations(); // ❌ Fails
});Environment
- HeroUI: v3.0.0-beta.3
- React: 19.2.1
- Next.js: 16.0.10
- Node: 22.0.0
- axe-core: 4.10.x
Expected Behavior
aria-controlsattribute should reference actual tabpanel IDs that exist in the DOM- Popover triggers should not have nested focusable elements
Workaround
Using React Aria Tabs directly until HeroUI v3 fix is available:
import { Tabs, TabList, Tab, TabPanel } from 'react-aria-components';
<Tabs defaultSelectedKey="availability">
<TabList>
<Tab id="availability">Availability</Tab>
<Tab id="attempts">Attempts</Tab>
</TabList>
<TabPanel id="availability">Content 1</TabPanel>
<TabPanel id="attempts">Content 2</TabPanel>
</Tabs>References
- WCAG 2.1: https://www.w3.org/WAI/WCAG21/quickref/#name-role-value
- axe-core aria-valid-attr-value: https://dequeuniversity.com/rules/axe/4.10/aria-valid-attr-value
- axe-core nested-interactive: https://dequeuniversity.com/rules/axe/4.10/nested-interactive
- React Aria Tabs: https://react-spectrum.adobe.com/react-aria/Tabs.html
Additional Context
This blocks production deployment for applications requiring WCAG 2.1 Level A accessibility compliance. React Aria (the foundation of HeroUI v3) has proper ARIA implementations - this appears to be a wrapper issue in HeroUI v3.
Source: heroui-inc/heroui