Mosmox UI
Current version
GitHubnpmjs.com
Navigation & Selection

Tabs

Accessible tab navigation with horizontal, vertical, selectable, and configurable layouts

Tabs organizes related content into switchable panels. It supports horizontal and vertical layouts, a quiet line treatment, controlled and uncontrolled state, keyboard navigation, disabled tabs, badges, icons, full-width layouts, manual activation, and lazy panel rendering. It is a presentational/client-side component: it does not fetch data, paginate records, or persist the active tab for you.

See the live Tabs showcase.

Basic usage

import { Tabs } from '@mosmox/ui';

<Tabs
  items={[
    { key: 'overview', label: 'Overview', content: <p>Summary content</p> },
    { key: 'activity', label: 'Activity', content: <p>Recent activity</p> },
  ]}
/>

The first non-disabled tab is active by default. Every item requires a stable key and label; content is rendered in its associated panel.

Sizes and orientation

Tabs use a line treatment with a text-color and thin underline accent for the active tab. There are no filled or pill-shaped active-tab variants. size accepts default, sm (small), or xs (extra small). orientation accepts horizontal or vertical.

<Tabs items={items} />
<Tabs items={items} size="sm" />
<Tabs items={items} orientation="vertical" />

fullWidth makes the tab list fill its container and distributes tabs across that width. fitted distributes each tab equally without changing the root width; use it when the parent already controls the tab area. When both are set, the list fills the container and the tabs share it equally.

Controlled and uncontrolled state

Use defaultActiveKey for local state or activeKey with onChange for controlled state.

<Tabs items={items} defaultActiveKey="activity" />

<Tabs
  activeKey={activeKey}
  items={items}
  onChange={(key, item) => {
    setActiveKey(key);
    analytics.track('tab-change', { key, label: item.label });
  }}
/>

Icons, badges, and disabled tabs

<Tabs
  items={[
    { key: 'files', icon: <Folder size={16} />, label: 'Files', badge: 12, content: <Files /> },
    { key: 'settings', label: 'Settings', content: <Settings /> },
    { key: 'billing', label: 'Billing', disabled: true, content: <Billing /> },
  ]}
/>

Keyboard activation

Tabs use roving tab focus. Left/Right arrows navigate horizontal tabs; Up/Down navigate vertical tabs; Home and End jump to the first or last enabled tab. With the default activationMode="automatic", moving focus also activates a tab. With manual, Enter or Space activates the focused tab.

<Tabs items={items} activationMode="manual" />

Lazy panels

lazy avoids rendering inactive panels. With lazy and keepMounted, inactive panels remain rendered in the DOM but stay hidden; without keepMounted, only the active panel is rendered. keepMounted has no effect unless lazy is also enabled.

<Tabs items={items} lazy />
<Tabs items={items} lazy keepMounted />

ConfigProvider

Set defaults for all Tabs inside a provider. Local props override provider values.

<ConfigProvider components={{ tabs: { size: 'sm', orientation: 'horizontal', fullWidth: true } }}>
  <Tabs items={items} />
</ConfigProvider>
SettingTypeDefaultDescription
fullWidthbooleanfalseMakes the tab list fill its container.
orientationhorizontal | verticalhorizontalTab list direction.
sizexs | sm | defaultdefaultTab density and text size. sm is small and xs is extra small.

API Reference

PropTypeDefaultDescription
itemsTabItem[]requiredTabs and their panel content.
activeKeystring—Controlled active tab.
defaultActiveKeystringfirst enabled tabInitial uncontrolled tab.
onChange(key, item) => void—Called after activation.
orientationhorizontal | verticalprovider value or horizontalTab layout direction.
sizexs | sm | defaultprovider value or defaultTab size. sm is small and xs is extra small.
activationModeautomatic | manualautomaticWhether focus activates a tab.
fullWidthbooleanprovider value or falseStretches tabs across the list.
fittedbooleanfalseMakes each tab share available space.
lazybooleanfalseDoes not render inactive panels.
keepMountedbooleanfalseKeeps lazy panels mounted after first render.
classNamestring—Additional root class.
styleCSSProperties—Additional root styles.

onChange receives the activated tab key and the complete TabItem. In uncontrolled mode it also updates the active panel; in controlled mode the parent must update activeKey in response to the callback.

TabItem

FieldTypeDescription
keystringStable unique identifier.
labelReactNodeVisible tab label.
contentReactNodeAssociated panel content.
iconReactNodeLeading icon.
badgeReactNodeBadge or count after the label.
disabledbooleanPrevents activation and keyboard focus.

Union values

TypeAllowed valuesMeaning
TabsOrientationhorizontal, verticalPlaces the tab list above the panel or to its left.
TabsSizedefault, sm, xsStandard, small, or extra-small tab density.
TabsActivationModeautomatic, manualActivates on focus, or only after Enter/Space.

Custom content

All visual content fields are ReactNode, so labels, icons, badges, and panels can be composed from application components rather than plain strings.

<Tabs
  items={[
    {
      key: 'activity',
      label: <span>Activity</span>,
      icon: <ActivityIcon aria-hidden="true" />,
      badge: <Badge>4</Badge>,
      content: <ActivityFeed projectId="atlas" />,
    },
  ]}
/>

Complete controlled example

import { useState } from 'react';
import { ConfigProvider, Tabs } from '@mosmox/ui';

const items = [
  { key: 'details', label: 'Details', content: <Details /> },
  { key: 'activity', label: 'Activity', content: <ActivityFeed /> },
  { key: 'settings', label: 'Settings', content: <Settings /> },
];

export function ProjectTabs() {
  const [activeKey, setActiveKey] = useState('details');

  return (
    <ConfigProvider components={{ tabs: { size: 'sm', fullWidth: true } }}>
      <Tabs
        activeKey={activeKey}
        activationMode="manual"
        items={items}
        onChange={(key) => setActiveKey(key)}
        orientation="horizontal"
      />
    </ConfigProvider>
  );
}

Accessibility

The component uses tablist, tab, and tabpanel roles, links each tab to its panel with aria-controls/aria-labelledby, and exposes the active state through aria-selected. Disabled tabs are removed from keyboard navigation. Panels remain keyboard focusable for users navigating content with a keyboard.