See Our Public Workshops:
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:
// some super minimal styles as a starting placeimport '@reach/tabs/styles.css'import { Tabs, TabList, Tab, TabPanels, TabPanel } from '@reach/tabs'function App() {return (<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.
<iframe width="100%" height="450" src="https://www.youtube.com/embed/iT3QKb9Y-FI" frameborder="0" allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>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<Tabsdata={[{ 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={[ ... ]} />
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 dataconst 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!