What Tabs does for you, and what it leaves to the page around it.
What you get for free
Tabs builds the full ARIA tab pattern itself. The kit only changes its colors: it adds no role and takes none away. What is left to you is naming, the disabled decision, and contrast.
The nav is role="tablist" and carries aria-orientation, set to horizontal for a top or bottom bar and vertical for start or end.
Each tab is a role="tab" element with aria-selected, aria-controls pointing at its panel and aria-disabled.
The tab list is one tab stop: the active tab has tabIndex=0 and the rest -1, the roving tabindex the pattern asks for.
Each panel is role="tabpanel" with aria-labelledby pointing back at its tab and aria-hidden while inactive.
The keyboard
Arrow keys move focus, not the selection. The panel does not change until you press Enter or Space. That is manual activation, the right default when a panel costs something to render.
Left and Right on a horizontal bar, Up and Down on a vertical one. The other pair is ignored, so a vertical bar does not swallow horizontal navigation.
Focus wraps at both ends, and it skips disabled tabs.
Home and End move focus to the first and last enabled tab.
Enter or Space activates the focused tab. With nothing focused yet it re-activates the current one, so neither key can leave the bar with no selection.
Backspace or Delete removes the focused tab when that tab is removable, and focus moves to a neighbor. Middle mouse button does the same thing with a pointer.
Under rtl on a horizontal bar Left and Right are swapped, so the arrow follows the visual direction rather than the array order.
A visually hidden aria-live="polite" region inside the focused tab announces “Tab N of M”, counted over the enabled tabs.
Naming tabs and panels
A tab's accessible name is its text content, which makes the icon-only tab the one case that goes wrong silently.
An icon-only tab keeps its word. Put it, sr-only, inside icon and leave label empty. In label the word names the tab just as well, but any label, even a hidden one, adds a gap after the icon.
The tab list has no name of its own. Tabs does not add one, and an aria-label passed to Tabs lands on the outer <div>, which carries no role, rather than on the role="tablist" element inside it. Where a page carries more than one bar, name the region around each one, with a visible heading wherever there is room for one.
Pass id if the first paint matters.aria-controls and aria-labelledby are both built from the Tabs id, and without one it is generated after mount, so neither attribute is there on the first render. Your own id is used as-is and the wiring exists from the first render.
A panel is named by its tab, so the tab's label is doing two jobs. Keep labels distinct: two tabs called “Details” leave two panels with the same name.
Focus management
The active panel is itself a tab stop (tabIndex=0), but only while it is active and has children. That is what lets a reader Tab off the bar into a panel with no focusable content of its own. Do not add a tabIndex of your own to the panel.
The remove button follows the active tab. It is tabIndex=0 on the active tab and -1 on the rest, so a bar of ten closable tabs is still two tab stops rather than twenty.
Removing a tab moves focus but not the selection. Focus moves to the next enabled tab, or to the previous one when the removed tab was last. The active key is yours: in a controlled bar, set a new activeKey in the same onEdit handler or the bar ends up with none.
The focus ring is antd's own and follows arrow navigation rather than selection, drawn around the focused tab. The neutral colors leave it alone, so do not replace it with a color change on the label, which is already showing the selection.
The overflow menu
The dropdown that holds the tabs which did not fit is a listbox, not a second tab list, and it has its own keyboard.
The trigger carries aria-haspopup="listbox", aria-controls and aria-expanded. Down, Space or Enter opens it; Up and Down walk the options, skipping disabled ones; Space or Enter activates the highlighted one; Esc closes it.
The menu itself is role="listbox" with role="option" children and aria-activedescendant, and it takes no tab stop of its own. The key handler is on the trigger, so focus never leaves the button while the menu is being driven.
Its three built-in strings are hardcoded English literals. The menu's aria-label falls back to “expanded dropdown”, the remove button's to “remove” and the add button's to “Add tab”. They do not come from the app locale, so in a non-English product set locale on the component with dropdownAriaLabel, removeAriaLabel and addAriaLabel.
On a mobile device the trigger is not rendered at all: the bar is scrolled by touch instead. Nothing may depend on the menu being reachable.
Disabled tabs
disabled sits on the item. It sets aria-disabled and removes the tab's tabIndex attribute entirely, so the tab is not focusable and arrow keys step over it.
Because it cannot be focused, a keyboard reader cannot land on it to find out why it is off. A disabled tab is a dead end with no explanation attached.
Prefer leaving the tab enabled and putting the reason in the panel: an empty state can say what unlocks the content, and a aria-disabled tab cannot.
A disabled label keeps antd's disabled color, which is below the text contrast minimum, as disabled text is allowed to be. Never let the gray be the only thing saying “unavailable”.
Contrast and motion
Under color="default" a resting label is colorTextSecondary and the selected one colorTextBase, so the selection is a step in text color plus the indicator rather than a hue change. That is the pair to keep: a restyle that flattens the two back together leaves the indicator carrying the state alone.
Under color="primary" the selected label is colorPrimary, so check a custom seed against the container it sits on. A hue picked for the brand can fall under 4.5:1 with nothing warning.
The hover surface on floating, padded and docked is a low-alpha fill. It is a pointer affordance and it does not exist for a keyboard reader, so it must never be the thing that says which tab is current.
animated defaults to { inkBar: true, tabPane: false }: the indicator slides, panels do not cross-fade. animated={false} turns both off, and animated={true} turns the panel motion on too, which is the setting to think twice about.
Tabs does not read prefers-reduced-motion. A reader who asked for less motion gets it only if you pass animated={false}, or turn motion off in the theme with token: { motion: false } on a ThemeScope, in both modes.
The indicator does not animate while the bar is driven from the keyboard, so arrowing through ten tabs does not chase a sliding bar. You lose that if you rebuild the bar through renderTabBar.