A Figma publication for design systems creators, designers, developers, and managers
Article

React containers, some assembly required

Author
Ryan Seddon
Ryan Seddon@ryanseddon
Software Engineer working on design systems at Zendesk.

The Zendesk Garden design system has used a pattern we’ve coined “containers” to help share common keyboard and accessibility (a11y) logic between many components. This greatly reduces our component code size as we reduce these containers to their raw forms so they can be used to build up other abstractions.

Currently the Zendesk Garden design system uses the render prop technique and is inspired by the downshift.js library that offers a very similar approach for highly accessible inputs.

I’ve written about the container pattern previously and why it’s an interesting approach. They render no UI, handle keyboard and mouse interaction and support RTL layouts.

I believe this pattern has a lot of merit but its previous incantation is buried in our high abstraction react components, meaning you have to bring down a package – styling included – just to use our no-UI containers.

Well no more! Introducing react-containers: a new open source library to help elevate these patterns into their own repo. We’ve re-written them to be smaller, smarter, and adhere more closely to the WAI-ARIA Authoring Practices 1.1.

We now expose these as hooks and render prop containers. The render prop containers are actually super light wrappers around the hook themselves.

function TabContainer({ children, render = children, ...options }) {
  return render(useTabs(...options); 
}

That’s all there is to it. All the complex UI logic is handled via an easy to use hook. Note that the render prop version gives you the alternative to use these hooks in a class component. We will go into this in more detail later on.

Why?

These containers are excellent building blocks to give anyone a head start in creating accessible, keyboard controlled and RTL aware components.

Not only does this speed up development time it also drastically improves the end-user experience by making your UI much more inclusive to all users. Inclusivity benefits everybody.

Assembling a simple Tab UI

We supply 12 containers that can help with building up your own design system or set of custom components. All you need to do is supply the visuals.

Let’s explore creating some tabs with our useTabs hook.

The main focus of this example is a single line that exposes the tab logic you need to apply to your elements.

const { selectedItem, focusedItem, getTabProps, getTabListProps, getTabPanelProps } = useTabs();

This returns an object with two items and three “prop getters” that you apply to the corresponding elements that make up your tab interface.

The two stateful items:

  • selectedItem is the currently selected tab with the default being the first tab.
  • focusedItem is the current keyboard focused tab.

The “prop getters” – a function that returns an object for you to spread onto elements:

  • getTabProps applies the specific a11y attributes required for each tab.
  • getTabListProps applies the tablist role attribute.
  • getTabPanelProps applies the specific a11y attributes required for each tab panel.
<button {...getTabProps({
  item: 'tab 1',
  index: 0,
  focusRef: React.createRef(null)
})>
  tab 1
</button>

The three required props that need to be applied are item, index and focusRef.

  • item is a unique name that can be a number or string – this is what selectedItem/focusedItem will return.
  • index is a number that allows the mapping of a tab to a panel when generating ids – usually just the current map index when looping over multiple tabs.
  • focusRef is a reference to the element that useTabs will call focus() on.

The other requirements of getTabPanelProps are fairly simple:

<div {...getTabPanelProps({ index: 0, item: 'tab 1' })} />

All it needs to function is index and item which should be the same thing as what was applied to the tab.

Better visual treatment

Think of these containers as the foundational level of your components. They get the important behaviours right; you can focus on the visual treatment. Let’s explore an improved visual treatment for the same code as the above example with styling changes only.

Beyond encapsulating styling using styled-components the render of our original component has changed very little.

A step further

I recently came across a neat example of an auto-generated UI based on defined design constraints called Uibot.app. What if we could use our containers as foundational building blocks in this auto UI generator? You end up with an accessible, keyboard navigable and RTL aware randomised design generator.

Again the code is all about the visual treatment. Behaviour and accessibility remain the same.

The switch from horizontal to vertical tabs is already handled via the hook, passed as an argument into the hook itself.

const { 
  selectedItem, 
  focusedItem,
  getTabProps,
  getTabListProps,
  getTabPanelProps 
} = useTabs({ vertical: true });

Internally this switches the keyboard controls to vertical mode and changes the aria-orientation to vertical.

As with vertical treatment, useTabs also seamlessly handles RTL switching and the keyboard controls respond accordingly.

const { 
  selectedItem, 
  focusedItem,
  getTabProps,
  getTabListProps,
  getTabPanelProps 
} = useTabs({ rtl: true });

The complexity of handling tabs is neatly captured in the hook. All the work is on the visual treatment and handling the states for that.

Using these containers as your base layer helps lift away what would be a complex component into something much simpler and easier to reason about.

What if I have existing class components?

If you have existing class components and don’t have the option of moving over to a function component that will work with these hooks, we also provide a render-prop container that can work nicely.

class Tabs extends React.Component {
  render() {
    return (
      <TabsContainer vertical={true}>
        {({ 
          selectedItem, 
          focusedItem,
          getTabProps,
          getTabListProps,
          getTabPanelProps 
        }) => (
          // Spread props and use state properties in your render
        )}
      </TabsContainer>
    );
  }
  }

Changing a hooks behaviour

Sometimes for product or UX reasons the behaviour of these containers might not work exactly as your users expect it to. Let’s explore the useTooltip hook and how we can stop the default behaviour on mouse enter.

The above demo will show a tooltip when the button receives focus or is clicked. We’ve suppressed the default mouse enter event.

<button {...getTriggerProps({ onMouseEnter: e => e.preventDefault() })}>
  Trigger
</button>

Any event that is applied internally in our hooks uses the composeEventHandlers utility which will execute every event in order until one of them calls e.preventDefault(). So in our case the hook’s internal onMouseEnter event is never triggered as it is cancelled by the user-supplied event.

Did you remember your ref?

One aspect to point out with these hooks is that it puts all the onus on the consumer to supply a ref to the hook and to attach it to the right element.

const tooltipRef = React.useRef(null);
const { isVisible, getTooltipProps, getTriggerProps } = useTooltip({
  tooltipRef
});

// render
<div
  {...getTooltipProps({
    ref: tooltipRef
  })}
/>

We wanted to keep the hooks as simple and as flexible as possible so we decided against creating refs internally and passing them back.

What’s next?

Documentation, documentation, documentation. We only have a storybook of the components right now which makes it hard to see how to use them and all of the available features.

The next task is to remove our old render-prop containers from react-components and dog food these as their replacements.

We hope you like this approach, and potentially can get some use out of them. Just like the rest of the Zendesk Garden design system, react-containers is open source and available on NPM for you to use now.

You can read more from the Zendesk Engineering team over on their blog.

Design + Development

Design + Development

Exploring collaborations in code
Mike Fix

White-labeling: Putting the design system in users' hands

Using 'styled-components` and `styled-system` to support customization within a 3rd party application while also preventing unwanted changes.

Ryan Seddon

React containers, some assembly required

Introducing react-containers: a new open source library to help elevate no-UI containers, with a focus on accessibility, brought to you by the Zendesk Design Systems team.

John Choura

Stack mirroring: Designing for code and coding for design

What modern design systems can learn by taking direction from how programming already functions.

Read all articles

Ready to Contribute?

The team at designsystems.com is always looking for interesting content. Share your ideas with us — we’d love to hear from you.

Subscribe for Updates

We will be rolling out new articles and guides on a regular basis. Sign up to get the latest delivered to you.

Design Systems

version 2.0.0