How To Build Tab Component React

A comprehensive guide to building accessible, production-ready tab components from scratch, with comparisons of popular library approaches.

Introduction

Tabs are one of the most common UI patterns in modern web applications. They allow users to navigate between different sections of content within the same container, improving organization and reducing page scrolling. Whether you're building a settings page, a product dashboard, or a multi-section form, tabs provide an intuitive way to present layered information without overwhelming users.

Building a tab component in React might seem straightforward at first glance, but creating one that is fully accessible, performant, and flexible requires attention to several important details. This guide walks you through building a production-ready tab component from scratch, understanding the accessibility requirements that make it work for all users, and exploring how different component libraries approach the same problem.

For teams working on object-oriented design patterns, understanding component architecture is essential for building maintainable interfaces.

Understanding Tab Component Anatomy

Before diving into implementation, it's essential to understand what makes up a tab component at the structural level. According to the WAI-ARIA Authoring Practices Guide, a tab component consists of "a set of layered sections of content, known as tab panels, that display one panel of content at a time."

The Anatomy of Tabs

The anatomy of a tabs component includes three primary elements working together:

Tab List: The container for all tab buttons, acting as the navigation control that users interact with to switch between panels. The tab list uses role="tablist" to announce its purpose to screen readers.

Individual Tabs: Buttons that activate their corresponding content panel and indicate their own active or selected state. Each tab uses role="tab" and must reference its associated panel through aria-controls.

Tab Panels: The content containers that display the actual content associated with each tab, with only one panel visible at any given time. Tab panels use role="tabpanel".

Understanding this separation between the navigation element (tabs) and the content element (panels) is crucial because it affects how you structure your HTML, manage state, and implement keyboard navigation. This architectural separation mirrors principles discussed in our guide on building color palettes with CSS, where visual and structural concerns are thoughtfully balanced.

DOM Structure and Accessibility Requirements

Proper accessibility starts with correct ARIA attributes and role assignments. The tab list container requires role="tablist" to announce its purpose to screen readers. Each tab button needs role="tab" and must reference its associated panel through aria-controls, which contains the ID of the corresponding tab panel element.

ARIA Attributes Reference

ElementRoleKey Attributes
Containertablistaria-label
Tab Buttontabaria-selected, aria-controls, tabindex
Content Paneltabpanelaria-labelledby, tabindex

The currently selected tab receives aria-selected="true", while others get aria-selected="false". Tab panels use aria-labelledby to reference their controlling tab through the tab button's ID.

tabindex Usage

The tabindex attribute plays an important role in keyboard usability. The active tab should have tabindex="0" to be focusable during keyboard navigation, while inactive tabs should have tabindex="-1" so they can receive focus programmatically but not interrupt the natural tab order of the page.

Example DOM Structure

Proper ARIA Tab Structure
1<div>2 <div role="tablist" aria-label="My Tabs">3 <button4 role="tab"5 id="tab-1"6 aria-selected="true"7 aria-controls="tabpanel-1"8 tabindex="0"9 >10 Tab 111 </button>12 <button13 role="tab"14 id="tab-2"15 aria-selected="false"16 aria-controls="tabpanel-2"17 tabindex="-1"18 >19 Tab 220 </button>21 </div>22 23 <div24 id="tabpanel-1"25 role="tabpanel"26 aria-labelledby="tab-1"27 tabindex="0"28 >29 Content of Tab 130 </div>31 <div32 id="tabpanel-2"33 role="tabpanel"34 aria-labelledby="tab-2"35 tabindex="0"36 >37 Content of Tab 238 </div>39</div>

Unique Identifier Requirements

The aria-controls and aria-labelledby attributes require unique identifiers throughout the entire document. This becomes complex when you have multiple tab components on a single page or when using server-side rendering where you cannot rely on auto-incrementing counters safely.

Modern React applications can use the useId() hook to generate consistent unique IDs across server and client renders. For tab components specifically, you might generate IDs like radix-:r1:-trigger-tab-1 that combine a generated prefix with the tab's value.

Key considerations for ID generation:

  • Use useId() for server-side rendered applications
  • Ensure IDs remain consistent between server and client
  • Avoid whitespace characters in tab values
  • Prefix IDs with the component name to prevent collisions

Building a Custom Tab Component

Let's build a complete tab component from scratch. We'll create a flexible implementation that supports both controlled and uncontrolled patterns, handles keyboard navigation, and manages ARIA attributes automatically.

State Management Approach

The first decision in building a tabs component is whether to use controlled or uncontrolled state:

Uncontrolled Component: The tabs manage their own state internally. Simpler to implement but less flexible for complex applications.

Controlled Component: The parent manages the active tab state. Provides more flexibility for complex applications and integration with other components.

Uncontrolled Tabs Implementation
1import { useState } from 'react';2 3function Tabs({ children, defaultValue }) {4 const [activeTab, setActiveTab] = useState(defaultValue);5 6 return (7 <div role="tablist" aria-label="My Tabs">8 {children.map((child) => (9 <button10 role="tab"11 aria-selected={child.props.value === activeTab}12 aria-controls={`panel-${child.props.value}`}13 tabindex={child.props.value === activeTab ? 0 : -1}14 onClick={() => setActiveTab(child.props.value)}15 >16 {child.props.label}17 </button>18 ))}19 </div>20 );21}

Controlled Component Pattern

For more complex applications, a controlled approach gives the parent component full control over which tab is active:

Controlled Tabs Implementation
1function Tabs({ children, value, onChange }) {2 return (3 <div role="tablist" aria-label="My Tabs">4 {children.map((child) => (5 <button6 role="tab"7 aria-selected={child.props.value === value}8 aria-controls={`panel-${child.props.value}`}9 id={`tab-${child.props.value}`}10 tabindex={child.props.value === value ? 0 : -1}11 onClick={() => onChange(child.props.value)}12 >13 {child.props.label}14 </button>15 ))}16 </div>17 );18}

Keyboard Navigation Implementation

Proper keyboard navigation is essential for accessibility. Users should be able to navigate between tabs using arrow keys when the tab list has focus:

  • ArrowRight / ArrowLeft: Move focus to the next or previous tab
  • Home: Move focus to the first tab
  • End: Move focus to the last tab

The keyboard navigation handler must be registered on each tab button, and it uses DOM traversal to find the next or previous tab element.

Keyboard Navigation Handler
1function TabButton({ onKeyDown, ...props }) {2 const handleKeyDown = (event) => {3 switch (event.key) {4 case 'ArrowRight':5 event.preventDefault();6 focusNextTab(event.currentTarget);7 break;8 case 'ArrowLeft':9 event.preventDefault();10 focusPreviousTab(event.currentTarget);11 break;12 case 'Home':13 event.preventDefault();14 focusFirstTab(event.currentTarget);15 break;16 case 'End':17 event.preventDefault();18 focusLastTab(event.currentTarget);19 break;20 default:21 onKeyDown?.(event);22 }23 };24 25 return <button {...props} onKeyDown={handleKeyDown} />;26}27 28function focusNextTab(currentTab) {29 const tabList = currentTab.parentElement;30 const tabs = Array.from(tabList.querySelectorAll('[role="tab"]'));31 const currentIndex = tabs.indexOf(currentTab);32 const nextIndex = (currentIndex + 1) % tabs.length;33 tabs[nextIndex].focus();34}35 36function focusPreviousTab(currentTab) {37 const tabList = currentTab.parentElement;38 const tabs = Array.from(tabList.querySelectorAll('[role="tab"]'));39 const currentIndex = tabs.indexOf(currentTab);40 const prevIndex = (currentIndex - 1 + tabs.length) % tabs.length;41 tabs[prevIndex].focus();42}

Comparing Library Approaches

Different component libraries take varying approaches to implementing tabs, each with trade-offs in flexibility, accessibility, and ease of use. For production applications, consider using established libraries that handle accessibility patterns correctly out of the box.

Material UI

Material UI's Tabs component takes a minimalistic approach with separate Tabs and Tab components. It provides keyboard navigation and sets the aria-selected attribute, but it does not include a TabPanel component. This means developers must manually render tab panels and establish the connections between tabs and panels.

Advantages: Complete control over panel structure and styling. Disadvantages: Must handle accessibility details yourself, including unique ID generation.

PrimeReact

PrimeReact's TabView component abstracts the tab and panel into a single TabPanel component. This simplifies the API--each TabPanel contains both the header and the content, making the JSX more intuitive. The component handles the complexity of separating tabs from panels during rendering.

Advantages: Simple API, consistent ID generation. Disadvantages: Less flexibility in component composition.

Radix UI

Radix UI takes the most composable approach with four separate components: Tabs.Root, Tabs.List, Tabs.Trigger, and Tabs.Content. Each Trigger is connected to its Content through a value prop rather than through DOM relationships.

Advantages: Highly flexible, value-based composition, sophisticated collection system. Disadvantages: More verbose API to learn.

Best Practices for Production Tab Components

When building or choosing a tab component for production use, consider these important factors:

Performance

Tab components should lazy-render their panels to avoid mounting unnecessary content. If each panel contains expensive components or data fetching, rendering all panels upfront can significantly impact initial page load time.

Responsive Design

Tab components must handle various screen sizes gracefully. Horizontal tab lists may need to scroll on narrow screens, or you might switch to a different pattern like an accordion for mobile devices. Consider how your React component architecture handles different viewport sizes.

Right-to-Left Support

If your application supports RTL languages, the tab component should adapt accordingly. Arrow key navigation should reverse direction in RTL layouts.

Disabled Tabs

Handle disabled tabs appropriately by preventing activation and skipping them during keyboard navigation. Visually indicate the disabled state and ensure proper ARIA attributes are set.

Animation

Smooth transitions between tabs can improve the user experience, but must be implemented carefully. Ensure screen readers are notified when content changes and respect reduced-motion preferences.

Conclusion

Building a tab component in React requires balancing simplicity with accessibility. Starting with a custom implementation helps you understand the underlying patterns and requirements. As your application grows, you might transition to a library that handles the complexity for you.

The key takeaways are to follow ARIA patterns for accessibility, implement proper keyboard navigation for power users, generate unique IDs consistently across server and client renders, and choose an approach that matches your application's complexity and requirements.

Whether you build from scratch or use a library, understanding these patterns ensures your tab components work well for all users, regardless of how they interact with your application.

For teams building complex React applications, investing in proper component architecture pays dividends in maintainability and user experience. Consider exploring our custom React development services to build accessible component libraries tailored to your needs. For applications integrating AI-powered features, our AI automation services can help you build intelligent, adaptive interfaces.

Frequently Asked Questions

What is the difference between controlled and uncontrolled tab components?

Controlled components have their state managed by a parent component through props like value and onChange. Uncontrolled components manage their own internal state. Controlled components offer more flexibility for complex applications, while uncontrolled components are simpler to implement.

How do I handle multiple tab components on one page?

Each tab component needs unique IDs for its tabs and panels. Use React's useId() hook to generate unique identifiers. Ensure that aria-controls and aria-labelledby references point to the correct elements within each component instance.

What keyboard shortcuts should tab components support?

Tab components should support ArrowLeft and ArrowRight for horizontal navigation, ArrowUp and ArrowDown for vertical orientation, Home to focus the first tab, and End to focus the last tab. Tab should move focus into and out of the tab list normally.

How do I make tab components accessible?

Use proper ARIA roles: tablist for the container, tab for buttons, and tabpanel for content panels. Set aria-selected on the active tab, aria-controls linking tabs to panels, and aria-labelledby linking panels to tabs. Implement keyboard navigation with arrow keys.

Should I use a library or build tabs from scratch?

For simple use cases, building from scratch helps you understand the patterns. For production applications with complex requirements, libraries like Radix UI, Material UI, or React Aria Components provide accessibility, keyboard navigation, and flexible styling out of the box.

Need Help Building Custom React Components?

Our team specializes in building accessible, performant React applications with custom component libraries tailored to your needs.

Sources

  1. LogRocket: How to build a tab component in React - Full implementation guide with code examples for accessible tab components

  2. Sandro Roth: The many ways to build a tabs component in React - Comprehensive comparison of library implementations and patterns