Skip to content

Announcing Reach UI Tabs


The latest member of the Reach UI family is here!

Tab interaces 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:

// some super minimal styles as a starting place
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:

// Maybe pass everything in as an array and let Tabs do the rendering
// 🙅 not @reach/tabs API
<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> }
  ]}
/>

// Or split it up a bit, but co-locate the tab and the panel information, 
// then let Tabs figure out where to render
// 🙅 also not @reach/tabs API
<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>
)

// now pass in a array as shown before
<DataTabs data={[ ... ]} />

A

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>
  )
}

// just a placeholder since it's not actually rendered,
// its props are used as data
const ColocatedTab = () => null

// and there we go!
<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!

Loading...

React Router

Michael Jackson and Ryan Florence create the React libraries that you use in your apps like React Router and Reach UI. All of our trainers are experts in React and JavaScript so let us share our knowledge with you and your team!

I Love React
© React Training 2019