Recipes

Common patterns and solutions for real-world use cases.

Dark Mode / Theming

Joyride's visual options make it easy to adapt the tour to your app's theme. Pass color values through the options prop:

import { useJoyride } from 'react-joyride';
 
function App() {
  const isDarkMode = useTheme(); // your theme hook
 
  const { Tour } = useJoyride({
    steps,
    run: true,
    options: {
      backgroundColor: isDarkMode ? '#333333' : '#ffffff',
      textColor: isDarkMode ? '#ffffff' : '#000000',
      primaryColor: isDarkMode ? '#ffffff' : '#000000',
      arrowColor: isDarkMode ? '#333333' : '#ffffff',
      overlayColor: isDarkMode ? 'rgba(0, 0, 0, 0.7)' : 'rgba(0, 0, 0, 0.5)',
    },
  });
 
  return <div>{Tour}</div>;
}

The color options:

OptionWhat it styles
backgroundColorTooltip background
textColorTooltip text
primaryColorPrimary button and beacon
arrowColorArrow fill (should match backgroundColor)
overlayColorOverlay backdrop

For finer control, use the styles prop to target individual elements. See Styles for all available keys.

See it in action

All demos on this site use theme-aware colors. Toggle the theme switcher in the header to see it.

Multi-Route Tours

Tours that span multiple pages or routes. The key challenge is navigating between pages while keeping the tour running.

The simplest approach — use before hooks to navigate before a step renders. Joyride waits for the target to appear via targetWaitTimeout.

import { useJoyride } from 'react-joyride';
import { useNavigate } from 'react-router-dom'; // or next/navigation, etc.
 
function App() {
  const navigate = useNavigate();
 
  const { Tour } = useJoyride({
    continuous: true,
    run: true,
    steps: [
      // Page A — multiple steps before navigating
      {
        target: '.dashboard-header',
        content: 'Welcome to the dashboard.',
      },
      {
        target: '.dashboard-stats',
        content: 'Here are your stats.',
      },
      // Navigate to Page B before step 3
      {
        target: '.settings-panel',
        content: 'Adjust your settings here.',
        before: () => {
          navigate('/settings');
          return new Promise(resolve => setTimeout(resolve, 300));
        },
        after: () => navigate('/dashboard'), // optional: go back when done
      },
      // Page B — another step
      {
        target: '.notifications-toggle',
        content: 'Enable notifications.',
      },
    ],
  });
 
  return <div>{Tour}</div>;
}

This works well when:

  • You have multiple steps per page
  • Navigation is straightforward (no complex back/forward logic)
  • You want the tour to feel linear

Using step.data and onEvent

For complex routing — bidirectional navigation, dynamic routes, or when each step maps to a different page — store route info in step.data and handle navigation in onEvent:

import { Joyride, EVENTS, STATUS, type EventData } from 'react-joyride';
 
const steps = [
  {
    target: '#home',
    content: 'Start here.',
    data: { next: '/page-a' },
  },
  {
    target: '#page-a',
    content: 'Page A content.',
    data: { previous: '/', next: '/page-b' },
  },
  {
    target: '#page-b',
    content: 'Page B content.',
    data: { previous: '/page-a' },
  },
];
 
function handleEvent(data: EventData) {
  const { action, status, step, type } = data;
 
  if (([STATUS.FINISHED, STATUS.SKIPPED] as string[]).includes(status)) {
    return;
  }
 
  if (type === EVENTS.STEP_AFTER) {
    const route = action === 'prev' ? step.data?.previous : step.data?.next;
 
    if (route) {
      navigate(route);
    }
  }
}
tip

Joyride automatically waits for the target element to appear in the DOM (controlled by targetWaitTimeout, default 1000ms). You don't need to manually poll or add extra delays for route transitions.

See it in action

The Multi Route demo shows the step.data pattern with dynamic step injection.

Modals & Portals

When tour steps target elements inside modals, the tooltip may render behind the modal due to z-index stacking. Use portalElement to render the tooltip outside the modal's DOM hierarchy.

Setup

Create a portal container at the root of your app:

// In your layout or root component
<div id="joyride-portal" />

Then pass it to Joyride:

const { Tour } = useJoyride({
  portalElement: '#joyride-portal',
  steps: [
    {
      target: '.modal-input',
      content: 'Fill in this field.',
      disableFocusTrap: true, // allow interaction with modal inputs
      before: () => {
        openModal();
        return new Promise(resolve => setTimeout(resolve, 300)); // wait for animation
      },
    },
    {
      target: '.modal-table',
      content: 'Review the data.',
      blockTargetInteraction: true, // prevent accidental clicks
    },
  ],
});

Key options for modals

OptionWhy
portalElementRenders tooltip outside the modal's DOM tree, avoiding z-index issues
disableFocusTrapAllows keyboard interaction with modal form inputs (Joyride's focus trap would otherwise capture Tab)
blockTargetInteractionPrevents clicks on interactive elements like tables or forms during the step
options.zIndexAdjust if the tooltip still appears behind the modal (default: 100)
before hookWait for modal open animations before showing the step
See it in action

The Modal demo shows tours inside both HeroUI and react-modal dialogs.