Tabs
A tab-switching component with controlled / uncontrolled modes, card / line appearances, tab closing and adding, and full keyboard navigation.
Basic usage
import { useState } from 'react'
import { LhTabs, LhTabPane } from '@lh-ui/react'
export default function Demo() {
const [value, setValue] = useState('ink')
return (
<LhTabs value={value} onChange={(name) => setValue(name)}>
<LhTabPane name="ink" label="Ink">
Ink wash, vivid and flowing.
</LhTabPane>
<LhTabPane name="danqing" label="Danqing">
Danqing coloring, gold and green brilliance.
</LhTabPane>
</LhTabs>
)
}
Tabs Props
| Prop | Description | Type | Options | Default |
|---|---|---|---|---|
| value | Name of the active tab (controlled) | string | — | — |
| defaultValue | Name of the active tab (uncontrolled default) | string | — | — |
| type | Appearance | 'line' | 'card' | — | 'line' |
| closable | Whether tabs are closable | boolean | — | false |
| addable | Whether to show the add button | boolean | — | false |
| tabPosition | Tab position | 'top' | 'bottom' | — | 'top' |
| sx | Shorthand layout props | SxProps | — | — |
TabPane Props
| Prop | Description | Type | Default |
|---|---|---|---|
| name | Unique tab identifier (used as a stable key; changing it dynamically is discouraged) | string | — |
| label | Tab title | ReactNode | — |
| disabled | Whether to disable | boolean | false |
| children | Tab content | ReactNode | — |
Tabs events
| Event | Description | Callback signature | Vue equivalent |
|---|---|---|---|
| onChange | Emitted when the active tab changes | (name: string) => void | @change |
| onRemove | Emitted when a tab is closed (tab visibility is controlled by the business side) | (name: string) => void | @remove |
| onAdd | Emitted when the add button is clicked | () => void | @add |
Passing value makes the component controlled; passing defaultValue uses the internal uncontrolled state. onChange fires in both modes. When the active tab is removed, uncontrolled mode automatically switches to an adjacent available tab (matching plus's getNameAfterRemove); controlled mode leaves it to the business side inside onRemove.
children must be LhTabPane elements directly (you may render them with .map, and a flattening Fragment wrapper is allowed, but they cannot be wrapped in a custom component) — the parent collects tab records by descending with Children.forEach, and a custom wrapper breaks collection.
Keyboard interaction
Follows the APG tabs pattern: when focus is on a tab button, ArrowRight / ArrowLeft (or ArrowDown / ArrowUp when vertical) move between enabled tabs and activate them, skipping disabled ones; Home / End jump to the first / last item; Delete / Backspace close the current tab (requires closable). Only the active tab has tabindex=0; the rest are -1 (roving tabindex).
Accessibility
The tab bar is role="tablist", tabs are role="tab" with aria-selected / aria-controls, and panels are role="tabpanel" with aria-labelledby; ids are generated by useId() and are SSR-stable. Inactive panels are removed from the accessibility tree with the hidden attribute.
ref
forwardRef<HTMLDivElement> — the root element. LhTabPane forwards HTMLDivElement.
Styles
import '@lh-ui/react/theme.css'
import '@lh-ui/react/style/tabs'