An accessible and easy tab component for ReactJS with full keyboard navigation and ARIA support
—
Flexible state management supporting both controlled and uncontrolled modes with comprehensive event handling and validation.
import { Tabs } from "react-tabs";React Tabs supports two distinct modes for managing which tab is currently selected.
/**
* Controlled mode: Parent component manages selectedIndex
* Requires both selectedIndex and onSelect props
*/
interface ControlledTabsProps {
selectedIndex: number;
onSelect: (index: number, last: number, event: Event) => boolean | void;
}
/**
* Uncontrolled mode: Component manages state internally
* Uses defaultIndex for initial selection
*/
interface UncontrolledTabsProps {
defaultIndex?: number; // Initial tab index (default: 0)
selectedIndex?: null | undefined; // Must be null/undefined for uncontrolled
}Controlled Mode Usage:
import { useState } from 'react';
import { Tabs, TabList, Tab, TabPanel } from 'react-tabs';
function ControlledExample() {
const [tabIndex, setTabIndex] = useState(0);
const handleTabSelect = (index, lastIndex, event) => {
console.log(`Switching from tab ${lastIndex} to tab ${index}`);
setTabIndex(index);
// Return false to prevent the tab change
// return false;
};
return (
<Tabs selectedIndex={tabIndex} onSelect={handleTabSelect}>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</TabList>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
<TabPanel>Content 3</TabPanel>
</Tabs>
);
}Uncontrolled Mode Usage:
function UncontrolledExample() {
const handleTabSelect = (index, lastIndex, event) => {
console.log(`Tab changed from ${lastIndex} to ${index}`);
};
return (
<Tabs defaultIndex={1} onSelect={handleTabSelect}>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
<Tab>Tab 3</Tab>
</TabList>
<TabPanel>Content 1</TabPanel>
<TabPanel>Content 2</TabPanel>
<TabPanel>Content 3</TabPanel>
</Tabs>
);
}Comprehensive event handling for tab selection changes with cancellation support.
/**
* Tab selection event handler
* @param index - The index of the newly selected tab
* @param lastIndex - The index of the previously selected tab
* @param event - The DOM event that triggered the selection
* @returns boolean | void - Return false to prevent tab change
*/
type OnSelectHandler = (
index: number,
lastIndex: number,
event: Event
) => boolean | void;
interface TabsProps {
/** Called when tab selection changes */
onSelect?: OnSelectHandler;
}Event Handler Examples:
// Basic logging
const handleSelect = (index, lastIndex, event) => {
console.log(`Switched from tab ${lastIndex} to tab ${index}`);
};
// Conditional prevention
const handleSelectWithValidation = (index, lastIndex, event) => {
// Prevent switching if form is dirty
if (hasUnsavedChanges && !confirm('You have unsaved changes. Continue?')) {
return false; // Prevents the tab change
}
// Save current tab state
saveTabState(lastIndex);
loadTabState(index);
};
// Event type handling
const handleSelectByEventType = (index, lastIndex, event) => {
if (event.type === 'click') {
console.log('Tab selected by click');
} else if (event.type === 'keydown') {
console.log('Tab selected by keyboard');
}
};Initial state configuration for uncontrolled mode with validation.
interface TabsProps {
/** Initial tab index (0-based) for uncontrolled mode */
defaultIndex?: number; // Default: 0
/** Focus the selected tab on initial render */
defaultFocus?: boolean; // Default: false
}Configuration Examples:
// Start with second tab selected and focused
<Tabs defaultIndex={1} defaultFocus={true}>
<TabList>
<Tab>First</Tab>
<Tab>Second (Initially Selected)</Tab>
<Tab>Third</Tab>
</TabList>
<TabPanel>First Content</TabPanel>
<TabPanel>Second Content</TabPanel>
<TabPanel>Third Content</TabPanel>
</Tabs>
// Dynamic default index based on URL or props
const getInitialTab = () => {
const hash = window.location.hash;
if (hash === '#settings') return 2;
if (hash === '#profile') return 1;
return 0;
};
<Tabs defaultIndex={getInitialTab()}>
{/* tabs and panels */}
</Tabs>Advanced state management patterns for persisting tab state across sessions.
Local Storage Integration:
function PersistentTabs() {
const [selectedTab, setSelectedTab] = useState(() => {
const saved = localStorage.getItem('selectedTab');
return saved ? parseInt(saved, 10) : 0;
});
const handleTabChange = (index) => {
setSelectedTab(index);
localStorage.setItem('selectedTab', index.toString());
};
return (
<Tabs selectedIndex={selectedTab} onSelect={handleTabChange}>
{/* tabs and panels */}
</Tabs>
);
}URL Synchronization:
import { useNavigate, useLocation } from 'react-router-dom';
function URLSyncedTabs() {
const navigate = useNavigate();
const location = useLocation();
const tabMap = ['overview', 'details', 'settings'];
const currentTab = tabMap.indexOf(location.pathname.split('/').pop()) || 0;
const handleTabChange = (index) => {
const tabName = tabMap[index];
navigate(`/dashboard/${tabName}`);
};
return (
<Tabs selectedIndex={currentTab} onSelect={handleTabChange}>
<TabList>
<Tab>Overview</Tab>
<Tab>Details</Tab>
<Tab>Settings</Tab>
</TabList>
<TabPanel>Overview Content</TabPanel>
<TabPanel>Details Content</TabPanel>
<TabPanel>Settings Content</TabPanel>
</Tabs>
);
}Built-in validation ensures proper component structure and prevents common errors.
/**
* Validation errors that may be thrown (development mode only):
* - Error: "Switching between controlled mode... and uncontrolled mode is not supported"
* - Error: Tab and TabPanel counts must match (enforced via React warnings)
* - Error: Only one TabList component allowed per Tabs container
*/
interface ValidationBehavior {
/** Validates equal number of Tab and TabPanel components */
validateStructure: boolean;
/** Prevents mode switching after initial render */
preventModeChange: boolean;
/** Ensures proper component nesting */
validateNesting: boolean;
}Common Validation Errors:
// ❌ This will throw an error - switching modes
function BrokenModeSwitch() {
const [isControlled, setIsControlled] = useState(false);
return (
<Tabs
selectedIndex={isControlled ? 0 : null} // Error: Cannot switch between controlled/uncontrolled
defaultIndex={isControlled ? null : 0}
>
{/* components */}
</Tabs>
);
}
// ❌ Another mode switching error
function AnotherBrokenExample() {
const [selectedIndex, setSelectedIndex] = useState(null);
// Later in the component lifecycle...
useEffect(() => {
setSelectedIndex(0); // Error: Cannot switch from uncontrolled to controlled
}, []);
return (
<Tabs selectedIndex={selectedIndex}>
{/* components */}
</Tabs>
);
}
// ❌ This will throw an error - mismatched counts
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanel>Panel 1</TabPanel>
{/* Missing second TabPanel */}
</Tabs>
// ✅ Correct usage
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanel>Panel 1</TabPanel>
<TabPanel>Panel 2</TabPanel>
</Tabs>Internal logic for determining and maintaining tab management mode.
/**
* Mode detection based on props
* @param selectedIndex - The selectedIndex prop value
* @returns 'controlled' | 'uncontrolled'
*/
type TabMode = 'controlled' | 'uncontrolled';
// Internal mode detection logic:
// - selectedIndex === null → uncontrolled mode
// - selectedIndex is number → controlled modeThe library automatically detects the intended mode based on props and maintains consistency throughout the component lifecycle.
Built-in prop validation in development mode ensures correct usage patterns.
/**
* Runtime validation features (development mode only):
* - Prop type validation using checkPropTypes
* - Component structure validation
* - Mode consistency checking
* - Tab/Panel count matching
*/
interface ValidationFeatures {
/** Validates prop types in development */
propTypeChecking: boolean;
/** Ensures equal Tab/TabPanel counts */
structureValidation: boolean;
/** Prevents controlled/uncontrolled mode switching */
modeConsistency: boolean;
/** Only active in NODE_ENV !== 'production' */
developmentOnly: boolean;
}Validation Examples:
// Development mode validation
if (process.env.NODE_ENV !== 'production') {
// These errors will be thrown in development:
// ❌ Mode switching error
<Tabs selectedIndex={someCondition ? 0 : null}>
{/* Error: Cannot switch between controlled/uncontrolled */}
</Tabs>
// ❌ Structure validation error
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanel>Panel 1</TabPanel>
{/* Error: Unequal Tab/TabPanel count */}
</Tabs>
}
// ✅ Error boundary for validation errors
function TabsWithErrorBoundary() {
return (
<ErrorBoundary fallback={<div>Tab configuration error</div>}>
<Tabs>
<TabList>
<Tab>Tab 1</Tab>
<Tab>Tab 2</Tab>
</TabList>
<TabPanel>Panel 1</TabPanel>
<TabPanel>Panel 2</TabPanel>
</Tabs>
</ErrorBoundary>
);
}Install with Tessl CLI
npx tessl i tessl/npm-react-tabs