React containers, some assembly required
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 thetablist
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 callfocus()
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.