I recently needed to add guided tours to our React app - you know, those very annoying multi-step popovers that highlight parts of the page and explain what they do, even if no user will ever read them. And people love it when there's no skip button, so they are forced to go through the whole tour.
A few years ago, I faced the same challenge in an Angular project - finding a good tour library was tough. Now, working with React, it seems not much has changed. Unless your needs are very simple, there still aren’t many solid options. Let’s take a closer look.
React Joyride
React Joyride is a relatively popular tour library for React, with over 400K downloads per week. It supports both popovers and beacons - beacons are small animated indicators placed near the target element to attract the user's attention. When clicked, they open the corresponding popover.
Currently, React Joyride hasn't been updated for nine months and isn't compatible with React 19. There is an unstable next
version with React 19 support, but it doesn't work reliably.
React Joyride uses inline styles; custom class names are not supported unless you override the default components. Maybe you use Tailwind or SCSS (like we do) in the rest of your app, which means you're forced to override the default components.
The spotlight effect - darkening everything around the target element - is done using CSS mix-blend-mode: hard-light
. The effect breaks in certain situations, such as dark mode or when your backdrop color contains a subtle shade of your brand color.
For positioning, React Joyride relies on react-floater, a library from the same developer that is not widely used outside of React Joyride. While you can customize popover positions, some options - like offsetting the beacon - are missing.
React Joyride doesn't handle asynchronous elements. If your tour steps target elements that load later or span multiple pages, you'll need to manage the tour state yourself. The multi-route demo uses a simple setTimeout()
, which isn't practical for real-world apps.
Shepherd
Shepherd is a tour library for vanilla JS, with wrappers for React, Vue, and Angular. It has about 130K downloads per week (30K for the React wrapper), making it less popular than React Joyride. Shepherd is written in Svelte and uses Floating UI for popover positioning. A paid license is required for commercial use.
The React wrapper, react-shepherd
, isn't compatible with React 19, but since the wrapper is minimal, you can use shepherd.js
directly to avoid this issue.
Shepherd provides a CSS file for default styles and supports custom class names, making it easier to integrate with your existing styles compared to inline styles. Its SVG-based backdrop is flexible and can highlight multiple elements at once.
The tour configuration allows you to define the title, text, and a list of custom buttons for the popover. For everything beyond the basics - like formatted text or a Step x of y
progress indicator - you will have to leave JSX behind and work either with HTML strings or plain HTML elements. Yikes 😣
Shepherd doesn't natively support waiting for async elements, but you can use the beforeShowPromise
property to delay a step until your condition is met.
Intro.js
Intro.js is a tour library for vanilla JS, with community-maintained wrappers for React and other frameworks (though some are unmaintained). Its popularity is similar to Shepherd, and it also requires a paid license for commercial use. Intro.js supports popovers and beacons (called hints).
It's built with VanJS (or actually a modified version of it), a small reactive UI framework, but handles positioning itself. Based on demos, its positioning isn't always as reliable as other libraries.
Styling is managed with a CSS file, and you can customize class names or choose from different themes. However, the default look feels a bit outdated.
Accessibility is limited. Popovers use the dialog
role but aren't properly labelled with aria-labelledby
or aria-describedby
. Buttons are implemented as links with role="button"
instead of actual button elements (I wonder why? 🤔) , and some controls (close button and progress indicators) lack accessible labels. There's also no focus trap, so keyboard users can tab through the entire app instead of staying within the dialog.
Alternative
A guided tour mainly needs two things: state management and positioning. By combining a library for each, you can build a flexible solution tailored to your needs.
For positioning, Floating UI is a solid choice. It's used by Shepherd and popular libraries like Radix UI (and its successor Base UI).
For state management, it highly depends on your requirements and how you handle state in the rest of your app. For simple tours, you can use local state with useReducer
or any state management library you already have in place, like Zustand or Jotai. For more complex cases, I recommend XState. With XState, you can not only handle the tour state but, thanks to its actor model, also implement async waiting for tour elements and call API endpoints to mark a tour as completed.
Conclusion
Finding a good tour library is challenging. Incompatibility with React 19 and poor accessibility are dealbreakers. More advanced needs, like multi-page tours, still require custom solutions. If you know of a great, popular tour library I missed, let me know.
For now, I chose a custom approach using XState and Floating UI. Wish me luck! 🍀