Skip to main content

Tabs

A tab-switching component with controlled / uncontrolled modes, card / line appearances, tab closing and adding, and full keyboard navigation.

Basic usage​

Basic usage
Ink wash blending, vivid spirit.
Fine brushwork, every detail visible.
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​

PropDescriptionTypeOptionsDefault
valueName of the active tab (controlled)string——
defaultValueName of the active tab (uncontrolled default)string——
typeAppearance'line' | 'card'—'line'
closableWhether tabs are closableboolean—false
addableWhether to show the add buttonboolean—false
tabPositionTab position'top' | 'bottom'—'top'
sxShorthand layout propsSxProps——

TabPane Props​

PropDescriptionTypeDefault
nameUnique tab identifier (used as a stable key; changing it dynamically is discouraged)string—
labelTab titleReactNode—
disabledWhether to disablebooleanfalse
childrenTab contentReactNode—

Tabs events​

EventDescriptionCallback signatureVue equivalent
onChangeEmitted when the active tab changes(name: string) => void@change
onRemoveEmitted when a tab is closed (tab visibility is controlled by the business side)(name: string) => void@remove
onAddEmitted when the add button is clicked() => void@add
Controlled / uncontrolled

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.

Child node restriction

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'