Authoring ARIA is hard.
Let’s stop doing that.

Glossary

ARIA
ARIA is shorthand for Accessible Rich Internet Application. ARIA is a set of attributes you can add to HTML elements that define ways to make web content and applications accessible to users with disabilities who use assistive technologies (AT).
accessibility
/ ækˌsɛs əˈbɪl ə ti /
noun: a physical, mental, cognitive, or other condition that interferes with, or limits a person's ability to engage in certain tasks or participate in typical daily activities and interactions.
disability
/ ˌdɪs əˈbɪl ɪ ti /
noun: the design of products, devices, services, or environments so as to be usable by people with disabilities.
Inclusive Design
/ ɪnˈklu sɪv / / dɪˈzaɪn /
compound noun: a design process in which a mainstream product, service or environment is designed to be usable by as many people as reasonably possible, without the need for specialized adaptions. Design for one, extend to many.
Universal Design
/ ˌyu nəˈvɜr səl / / dɪˈzaɪn /
compound noun: Universal Design is the design and composition of an environment so that it can be accessed, understood and used to the greatest extent possible by all people regardless of their age, size, ability or disability
Disability Inclusion
/ ˌdɪs əˈbɪl ɪ ti / / ɪnˈklu ʒən /
compound noun: creating equitable experiences for people with disabilities

The Digital Accessibility Team has recently started publishing JavaScript packages under a @nike/web-a11y scope. These packages aim to assist both internal and external experience teams as well as our design systems by providing indifferent components that weave inclusive design patterns into your application. Most notably, our inaugural release is a Tabs component built atop react-aria by Adobe. In this post we’ll examine how you’ll use the Tabs component to create any type of tab design you can imagine. Most notably, we won’t set a single ARIA attribute ourselves.

Imagine we’re creating some membership tabs. Each tab has a title, which is the text for the persistent interactive “button” as well as tab content, which is conditionally rendered for the active tab.

import { TabSet, Tab } from "@nike/web-a11y-react-aria";

const MyComponent = () => {
  return (
    <TabSet title="Account Sections">
      <Tab id="profile-tab" title="Profile">
        <Profile />
      </Tab>
      <Tab id="inbox-tab" title="Inbox">
        <Inbox />
      </Tab>
      <Tab id="orders-tab" title="Orders">
        <Orders />
      </Tab>
      <Tab id="favorites-tab" title="Favorites">
        <Favorites />
      </Tab>
      <Tab id="settings-tab" title="Settings">
        <Settings />
      </Tab>
    </TabSet>
  );
};

So that’s pretty straight forward. We call the above signature our HTML Signature as it offers flexibility through markup. If that’s not your thing, perhaps the Prop–driven pattern better meets your needs. To translate the above example into something more JSON–esque we leverage our trusty data prop:

const MyComponent = () => {
  return (
    <TabSet
      title="Account Sections"
      data={[
        {
          id: "profile",
          title: "Profile",
          children: <Profile />,
        },
        {
          id: "inbox",
          title: "Inbox",
          children: <Inbox />,
        },
        {
          id: "orders",
          title: "Orders",
          children: <Orders />,
        },
        {
          id: "favorites",
          title: "Favorites",
          children: <Favorites />,
        },
        {
          id: "settings",
          title: "Settings",
          children: <Settings />,
        },
      ]}
    />
  );
};

When you’ve got formatted data ready to go, nothing beats a self–closing tag. With creative freedom and experimentation in mind, these flexible markup–or–data based signatures can be expected and found in all components we publish.

At this point you might be thinking those are some clever patterns and all, but how would I hook up analytics? How do I know when a tab is activated? How do I style the active tab differently? What if I need to place something in between the “button bar” (aka tablist) and the active tab panel? Must titles be a simple string?

Complex Titles

The most straightforward way to set a title for your tabs is via the title prop as demonstrated above. If you need a more complex title, like a span of some kind, leverage the <title> pattern as follows:

<TabSet>
  <title><span className="my-class">My&nbsp;Tabs</span></title>
  <Tab id="foo" title="Foo"><Foo /></Tab>
  <Tab id="bar" title="Bar"><Bar /></Tab>
</TabSet>

The same goes for the <Tab> component. Either a title prop or a <title> element can be used:

<TabSet>
  <title><span className="my-class">My&nbsp;Tabs</span></title>
  <Tab id="foo" title="Foo">
    <title><span>Foo</span></title>
    <Foo />
  </Tab>
  <Tab id="bar" title="Bar">
    <title><span>Bar</span></title>
    <Foo />
  </Tab>
</TabSet>

Responding to Tab Selection

Simply leverage the onTabSelected callback to stay in the know.

<TabSet
  onTabSelected={(tab) => {
    console.log(tab); // tab is a string (id of tab)
  }}
/>

The onTabSelected callback is a great place to fire off telemetry events for analytics.

Styling the Active Tab

You might be expecting something like a .is-active CSS class. We won’t be using a CSS class because we don’t need to. One of the great thing about inclusive design patterns is they often provide styling hooks naturally. In this case, we tap into the aria-selected attribute like so:

[role="tablist"] {
  > [role="tab"][aria-selected="true"] {
    /* active tab styles here */
  }
}

Pretty cool right?

See Also

Untethered Creative Freedom

There’s basically one limitation of <TabSet />. It controls the relationship between the tablist and active tabpanel…you can’t put anything in between them or otherwise separate them. Perhaps you need to do just that for a new A/B test. The useTabs() Hook can be leveraged to unleash absolute creative freedom like so:

const TabsHook = () => {
  const tabs = [
    {
      id: "profile",
      title: "Profile",
      children: <h2>Profile</h2>,
    },
    {
      id: "orders",
      title: "Orders",
      children: <h2>orders</h2>,
    },
    {
      id: "inbox",
      title: "Inbox",
      children: <h2>inbox</h2>,
    },
    {
      id: "favorites",
      title: "Favorites",
      children: <h2>favorites</h2>,
    },
    {
      id: "settings",
      title: "Settings",
      children: <h2>settings</h2>,
    },
  ];
  const [activeTab, TabBar, TabPanel] = useTabs(tabs, "My Tabs", {
    // optional properties
    orientation: VERTICAL,
    activeTab: "favorites",
  });

  const someOtherCustomStuff = <h1>Hello World {activeTab}</h1>;

  return (
    <div id="my-custom-tabs-implementation">
      {TabBar}
      {someOtherCustomStuff}
      {TabPanel}
    </div>
  );
};

Note in the above example we can sandwhich custom content in between the TabBar and TabPanel.

Abstracting ARIA away

As we conclude, take a moment to skim through each of the above examples. Count the number of times we manually set an aria- attribute for our tabs pattern.
Hint: we never did!

Fire up the TabSet storybook stories, or skim the HTML code block below taken from the HTML Signature story. Note the HTML structure and many attributes.

<div
  data-nr-web-a11y-component="TabSet"
  data-nr-web-a11y-style="true"
  data-nr-web-a11y-orientation="horizontal"
  data-nr-web-a11y-style-loaded="true"
>
   <div
     aria-label="Tab Title"
     id="react-aria9200098986-9"
     role="tablist"
     aria-orientation="horizontal"
   >
      <div
        tabindex="-1"
        data-key="inspirations"
        id="react-aria9200098986-9-tab-inspirations"
        aria-selected="false"
        role="tab"
      >
        <span>Inspirations</span>
      </div>
      <div
        tabindex="0"
        data-key="your-designs"
        id="react-aria9200098986-9-tab-your-designs"
        aria-selected="true"
        role="tab"
        aria-controls="react-aria9200098986-9-tabpanel-your-designs"
      >
        <span>Your Designs</span>
      </div>
   </div>
   <div
     id="react-aria9200098986-9-tabpanel-your-designs"
     aria-labelledby="react-aria9200098986-9-tab-your-designs"
     tabindex="0"
     role="tabpanel"
   >
      <p>Agnes Caroline Thaarup Obel is a Danish singer/songwriter. Her first album, Philharmonics, was released by PIAS Recordings on 4 October 2010 in Europe. Philharmonics was certified gold in June 2011 by the Belgian Entertainment Association (BEA) for sales of 10,000 Copies.</p>
   </div>
</div>

Note the attributes on > [role="tablist"]. The aria-label, role, and aria-orientation attributes have been automatically set for us. Moving into the > [role="tablist"] > [role="tab"] elements note the tabindex, id, aria-selected, role, and aria-controls attributes are being set for us. Lastly, our > [role="tabpanel"] is decorated with role, tabindex, id, and aria-labelledby attributes.

By comparing our React examples to the more complex HTML that is generated we can appreciate the complex tabs pattern that is wired up and managed for us. With these many attributes spanning across three levels of elements, there’s an awful amount of room for human error. By abstracting this responsibility away, we can rest assured our inclusive patterns are as resilient as possible.

Thank you for taking the time to have a look at TabSet and explore some new ways to bake inclusive design patterns into our components. These are just a few scattered examples of what is possible with React, the @nike/web-a11y tools, and modern web standards. As you author components with these tools look for similar ways to abstract away responsibilities and improve legibility while keeping signatures flexible all the while.

See Also