Library / SDK
reactjs/react-tabs avatar
reactjs/react-tabs

react-tabs: the four component tab set that kept its accessibility contract

An accessible and easy tab component for ReactJS.

3,146 stars455 forksJavaScriptMIT

At a glance

What is it?
A small React tab component split into Tabs, TabList, Tab and TabPanel, where the interesting decisions are all about controlled state, focus handling and what you have to remember to import.
Who is it for?
Pick react-tabs when you want a tab set that behaves correctly under a keyboard before you want one you can restyle into a particular look. It gives you controlled and uncontrolled modes, arrow key navigation, a cancel path from onSelect and proper ARIA wiring without extra dependencies, and the styling is a plain stylesheet you can override with class name props.
Can I use it commercially?
Yes. MIT is a permissive licence: you can use, modify and sell software built on it, as long as you keep its copyright and licence notices.
Is it still maintained?
Yes. The repository received new commits within the last day.
What is it written in?
Mainly JavaScript, according to GitHub's language statistics.

Answers come from the project's GitHub data, last synced on October 9, 2026, and from our analysis. They are not legal advice.

Editorial analysis

Four components that only work together

The first structural decision is that this is not a single component with a `tabs` prop. It exports exactly four, and the README states they all need to be used together. `Tabs` renders the outer div and holds state, `TabList` renders a `ul`, `Tab` renders an `li` for each label, and `TabPanel` renders the content region. The names match the markup, which means you can style against semantic list markup instead of fighting a div soup.

js
import { Tab, Tabs, TabList, TabPanel } from 'react-tabs';
import 'react-tabs/style/react-tabs.css';

export default () => (
  <Tabs>
    <TabList>
      <Tab>Title 1</Tab>
      <Tab>Title 2</Tab>
    </TabList>

    <TabPanel>
      <h2>Any content 1</h2>
    </TabPanel>
    <TabPanel>
      <h2>Any content 2</h2>
    </TabPanel>
  </Tabs>
);

Note the second line. The stylesheet is a separate import and the component does not inject it, which is the single most common source of an unstyled tab strip. Installation is the other line you will want:

bash
npm install --save react-tabs

The README also gives `yarn add react-tabs` for yarn users. Which command to run is not the interesting part; the version constraint on React is. Version 5 and newer of react-tabs needs React 18 or newer, and version 4 needs React 16.8 or newer.

Controlled and uncontrolled modes, and the rule that trips people up

The component keeps the selected index internally by default, which the README calls uncontrolled mode. If you pass `selectedIndex`, it switches to controlled mode, and in that mode `onSelect` becomes a required prop. The README is unusually clear about why: in controlled mode the component still handles focus of the tabs internally, so it needs `onSelect` in order to know what to do.

The consequence is that you cannot mix them freely. `defaultIndex` sets the initially open tab and is explicitly documented as usable only in uncontrolled mode, and it cannot be used together with `selectedIndex`. Both are zero based, so the first tab is `0`. If you find yourself wanting a default tab that also responds to a change from a parent component, that is the moment to commit to controlled mode and drive everything through `selectedIndex` and `onSelect`.

`onSelect` receives three arguments: the index being moved to, the index that was previously selected, and the underlying event, which is usually a keydown or a click. When the two indices are equal, the user clicked the tab that is already active. The handler can return `false` to cancel the change, and the README notes this matters in controlled mode too, because focus is still handled internally.

That cancel path is the feature that makes this component usable in a form, where a tab switch might need to run validation or confirm a navigation first. Most tab components give you an after the fact callback; this one gives you a veto.

Focus behaviour has two separate switches and one trap

Two props govern focus, and they do different things. `defaultFocus` defaults to false, and setting it to true focuses the tab list on initial render so keyboard navigation works immediately rather than requiring a tab press first.

`focusTabOnClick` defaults to true, meaning a clicked tab also takes DOM focus. The README attaches an explicit warning to turning it off: keyboard navigation will not work after a click, although you can still focus the tabs by pressing tab and then keyboard navigation will work. That is a real accessibility trade rather than a style preference, so it deserves a decision rather than a default.

The trap is `disableUpDownKeys`, which defaults to false and turns off the up and down arrow keys for changing tabs. The naming is slightly misleading in that it does not disable left and right; it only covers the vertical pair.

The other focus related detail is `domRef`, a callback that receives the underlying DOM node on every mount and null on unmount. That is what you need if you want to measure the tab list, scroll a selected tab into view, or drive focus from outside the component.

For right to left interfaces there is a `direction` prop taking `rtl` or `ltr`, defaulting to `ltr`. It exists because arrow key semantics have to follow reading order, and getting that wrong in an RTL layout is a bug users notice immediately.

Panel rendering, class names, and other props that pass through

By default only the active `TabPanel` is rendered to the DOM. `forceRenderTabPanel`, defaulting to false, changes that so all panels are always rendered, and the same behaviour is available per panel through a `forceRender` prop on an individual `TabPanel`.

The per panel option is the more useful of the two, and it exists because the always render case usually only matters for one panel. If your tab content holds a live chart, a video, or a form with unsaved input, rendering it always keeps that state alive across switches. If you render everything always, you pay for every panel on first paint, which is the trade the default is making for you.

Styling goes through class name props, and they are more flexible than the prop names suggest. `className` on `Tabs`, `TabList` and `Tab` accepts a string, an array of strings, or an object keyed by name with boolean values, following the convention from the `classnames` package that the README links to. There are also dedicated props for the selected and disabled states: `selectedTabClassName`, `disabledTabClassName`, `selectedTabPanelClassName` and `disabledClassName`, with the defaults you would expect such as `react-tabs__tab--selected`.

Unknown props are forwarded rather than swallowed. Additional props on `Tabs` go to the rendered `div`, on `TabList` to the `ul`, and on `Tab` to the `li`. That is convenient for `id`, `aria-*` or event handlers, though it also means a typo becomes a silent DOM attribute instead of an error, so `aria-label` on a `TabList` is worth writing explicitly rather than hoping it is inferred.

Where this fits against Material UI and the design system route

The obvious comparison is MUI's Tabs, and the practical difference is where the opinion lives. MUI ships a design system: opinionated defaults, a theme object, ripple and transition behaviour, and a large dependency. react-tabs ships a behaviour layer with a stylesheet you override, and nothing else. For an existing product with its own visual language, the second is often the smaller decision.

There is a real ergonomic gap between them. react-tabs has no scrollable or overflow tab strip, which you will notice the moment you have more than about six tabs, and there is no built in animation or transition between panels. Those are the two features most often reached for, and both require work outside this package.

The GitHub repository for this project also shows the maintenance picture. `package.json` declares `react` as a peer dependency of `^18.0.0 || ^19.0.0`, the package is published with both CommonJS and ES module builds, `index.d.ts` carries the types, and the build is Babel based with `vitest run` as the test command. Version 6.1.1 was published on 2026-03-25, and the last push was on 2026-09-22. The package is MIT licensed, and the `style/` directory at the repository root is what the `react-tabs/style/react-tabs.css` import resolves to.

Editorial conclusion

Pick react-tabs when you want a tab set that behaves correctly under a keyboard before you want one you can restyle into a particular look. It gives you controlled and uncontrolled modes, arrow key navigation, a cancel path from onSelect and proper ARIA wiring without extra dependencies, and the styling is a plain stylesheet you can override with class name props. It is not the right choice when you need scrollable or overflow tab strips, animations, or lazy panels with placeholders, since none of those are in this repository. Install it, read the controlled versus uncontrolled section of the README before wiring up state, and remember the stylesheet import is not automatic.

Frequently asked questions

How do I install react-tabs?

Run npm install --save react-tabs, or yarn add react-tabs, then import the stylesheet from react-tabs/style/react-tabs.css alongside the components. The import is required, because the component does not inject its own styling.

What React version does react-tabs need?

Version 5 and newer of react-tabs needs React 18 or newer, while version 4 needs React 16.8 or newer. The package declares react as a peer dependency of ^18.0.0 || ^19.0.0.

How do I control which tab is selected in react-tabs?

Pass selectedIndex, which is zero based, and that puts the component into controlled mode, where onSelect becomes a required prop. Leaving onSelect out in controlled mode breaks focus handling, and defaultIndex cannot be combined with selectedIndex.

How do I stop a tab from changing in react-tabs?

Return false from your onSelect handler. It receives the index being moved to, the previously selected index and the event, and returning false cancels the change. This also matters in controlled mode because the component handles tab focus internally.

Official sources

  1. License: MIT
  2. Project website
  3. reactjs/react-tabs on GitHub
  4. README
  5. Releases
Add this badge to your README

If you maintain this project, the badge below links readers to this analysis and shows its maintenance status from the daily GitHub snapshot. Paste the markdown into your README; add ?metric=license or ?metric=stars to the image URL for a different field.

Add this badge to your README

markdown
[![Hysen Labs](https://hysenlabs.com/badge/reactjs-react-tabs.svg)](https://hysenlabs.com/projects/reactjs-react-tabs)