Recently, one of the goals at Boords was finding a nice way of introducing some of the core features of our app to new users, so we decided to go with a guided-tours approach next to the usual help articles and tutorial videos.
The basic idea is to highlight a UI element and show a popover (i.e the little overlay with text) next to it with some introductory text. Simple.
There are some well-used guided tours libraries for React, like React Joyride and Reactour, but in the end these had the wrong feature set for what we wanted to do and did not feel very native to the way React apps work.
Here’s an example from Reacttour:
const steps = [
{
selector: '[data-tour="my-first-step"]',
content: () => (
<div>
Lorem ipsum
</div>
),
position: 'top',
position: [160, 250],
style: {
backgroundColor: '#bada55',
},
},
// ...
]
Both Reacttour and React Joyride use selectors to target components in the app (.upload_button
). As someone who works on React apps on a daily basis, this feels rather archaic. I feel strongly that this is the wrong approach 1 and feels prone to bugs in complex apps 2.
Being the small team that we are at Boords, I’m usually hesitant to custom-build stuff like this, but I was curious to see if I could add guided tours to our app without using selectors. In the end, I came up with a solution that:
- Feels native to React
- Allows waiting for specific actions
- Works across different screens
- Allows for callbacks in the context where the popover will be shown
- Is typesafe where possible
So how do we attach the Tour’s popover elements to the components they need to highlight? By wrapping the target elements in a React component that listens to the tour’s current step, and conditionally renders the popover and a highlight (as seen in the screenshot above).
The hintable
component
<TourHintable step="openEditor">
<FrameImage />
</TourHintable>
This is what our component looks like in its most basic form. When the step openTheEditor
is not active, it will only render its children (i.e. the component we’re wrapping), otherwise it will create a wrapper div
which also contains a blue overlay and the popover component (in this case, this is Boords’ typical popover element powered by Popper).
Linking the component to the step rather than the other way around also gives another neat benefit; multiple TourHintable
elements can respond to the same step. This is great when you want to highlight related elements (a canShowText
property can hide the flyover for all except one instance in this case).
It was important to me that the text itself was not defined as props to TourHintable
, because it would make reviewing the copy a nightmare.
This is a really neat separation of concerns. All the visual options (such as the popover position) are defined in the actual context in which the component is displayed, rather than in the place that defines the individual steps of the tour. This is something other solutions don’t do.
A tour
This is what a tour’s definition looks like in our app:
export const editingTour = {
name: 'editing',
onComplete: () => {
// Pop the champagne, fire confetti, or do nothing
};
steps: [
{
name: 'openFrame',
waitsOn: tourEvents.openFrameEditor,
header: `