The latest member of the Reach UI family is here!

Tab interfaces are incredibly common on the web, and really easy to build a naive version in a few lines of code. Have some state, click a tab, show a panel. But to make them accessible to keyboard and assistive tech users, and also flexible enough for lots of use-cases is a bit more involved.

Check it out:

import "@reach/tabs/styles.css" import { Tabs , TabList , Tab , TabPanels , TabPanel } from "@reach/tabs" < Tabs > < TabList > < Tab > Taco </ Tab > < Tab > Burrito </ Tab > < Tab > Taquito </ Tab > </ TabList > < TabPanels > < TabPanel > { tacoText } </ TabPanel > < TabPanel > { burritoText } </ TabPanel > < TabPanel > { taquitoText } </ TabPanel > </ TabPanels > </ Tabs >

As with all components in Reach UI, the primary objectives are accessibility and composability.

Accessibility

The tabs follow the WAI-ARIA practices described here . Each of the elements has the proper role so its announced to assistive tech users correctly, and has the right keyboard events as well. It even handles the tricky bits like skipping disabled tabs when using the keyboard.

Composability

A lot of tab components out in the wild (and that I've written myself) have an API similar to these two:

< Tabs data = { [ { label : 'Taco' , content : < p > all of the content </ p > } , { label : 'Burrito' , content : < p > all of the content </ p > } , { label : 'Taquito' , content : < p > all of the content </ p > } ] } /> < Tabs > < Tab label = " Taco " > < p > All of the content </ p > </ Tab > < Tab label = " Burrito " > < p > All of the content </ p > </ Tab > < Tab label = " Taquito " > < p > All of the content </ p > </ Tab > </ Tabs >

The problem with these APIs is that they aren't very composable.

In the first example, imagine we needed to add a className to every rendered tab. The API doesn't allow for that unless we start adding a bunch of weird props like tabClassName (which ultimately ends with tabProps={{ ... }} , and then gets weirder when you need the index so its like tabProps={index => ({ ... })} ).

In the second example we have a bit more control over rendering, but what if we needed the tabs on the bottom instead of the top? Or what if we wanted tabs on the top and bottom? We're hosed.

To be more composable, Reach UI Tabs uses a pattern that React Training coined in our workshops a few years ago: Compound Components.

(No, that doesn't mean components that are static members of other components, I have no idea where that idea got spread 😬, if this conversation interests you, we have a free course where this topic is covered extensively.)

Tab Components Map To Real Elements

Whenever you see <Tab/> it actually wraps a <button/> . So any props you pass to it go to the div because it is a button ! (And yes, disabled will do the right thing). Likewise, Tabs , TabPanels etc., all render a real DOM element, too. So you can treat them all like a normal HTML hierarchy. You're in charge of rendering--making Tabs maximally composable.

Remember the two problems before? It's easy now: we just put a className on the Tab and render the tabs on the bottom.

< Tabs > < TabPanels > < TabPanel > { tacoText } </ TabPanel > < TabPanel > { burritoText } </ TabPanel > < TabPanel > { taquitoText } </ TabPanel > </ TabPanels > < TabList > < Tab className = " taco " > Taco </ Tab > < Tab > Burrito </ Tab > < Tab > Taquito </ Tab > </ TabList > </ Tabs >

You can also easily create those other APIs on top of this:

const DataTabs = ( { data } ) => ( < Tabs > < TabList > { data . map ( ( tab , index ) => ( < Tab key = { index } > { tab . label } </ Tab > ) ) } </ TabList > < TabPanels > { data . map ( ( tab , index ) => ( < TabPanel key = { index } > { tab . content } </ TabPanel > ) ) } </ TabPanels > </ Tabs > ) < DataTabs data = { [ ... ] } />

And the co-located API:

const CoLocatedTabs = ( { children } ) => { return ( < Tabs > < TabList > { React . Children . map ( children , ( child ) => ( < Tab > { child . props . label } </ Tab > ) ) } </ TabList > < TabPanels > { React . Children . map ( children , ( child ) => ( < TabPanel > { chid . props . children } </ TabPanel > ) ) } </ TabPanels > </ Tabs > ) } const ColocatedTab = ( ) => null < ColocatedTabs > < ColocatedTab label = " Taco " > < p > All of the content </ p > </ ColocatedTab > < ColocatedTab label = " Burrito " > < p > All of the content </ p > </ ColocatedTab > < ColocatedTab label = " Taquito " > < p > All of the content </ p > </ ColocatedTab > </ ColocatedTabs >

Docs

Go check out the docs and if your tabs aren't accessible and composable, go replace them with our free labor today!