react-tourlight

Async Elements

Handle dynamically loaded content, lazy components, and elements that appear after mount.

In real applications, target elements aren't always present in the DOM when a tour starts. They might be rendered by a lazy-loaded component, fetched from an API, or appear after a user interaction. react-tourlight handles this automatically.

How it works

The player waits for a connected, visible target with a measurable layout box. Its MutationObserver watches for added or removed elements and attribute changes. A short polling interval also checks ref updates and layout changes that do not produce a DOM mutation.

Waiting ends when a target is ready, the configured timeout expires, or the guide is cancelled. After a step appears, target tracking updates the spotlight as its element moves or is replaced.

Automatic waiting

No extra configuration is needed. If a step targets a CSS selector and the element isn't found immediately, react-tourlight waits for it:

const steps: SpotlightStep[] = [
  {
    target: '#dashboard',
    title: 'Dashboard',
    content: 'Your overview is here.',
  },
  {
    // This element is rendered by a lazy-loaded component.
    // react-tourlight will wait for it to appear.
    target: '#analytics-chart',
    title: 'Analytics',
    content: 'Your metrics, visualized.',
    placement: 'bottom',
  },
]

Configuring timeout

By default, react-tourlight waits up to 5 seconds for a target element to appear. If the element doesn't appear within the timeout, the step is skipped and the tour advances to the next step.

Set timeout on an individual step, or waitForElementTimeout on the provider, to adjust this window. You can also prepare application state in onBeforeShow before target resolution begins:

{
  target: '#heavy-data-table',
  title: 'Data Table',
  content: 'All your records in one place.',
  onBeforeShow: async () => {
    // Wait for the data to load before showing this step.
    // The element observer won't start until this resolves.
    await waitForDataLoad()
  },
}

Using when predicates as an alternative

For elements that may or may not exist depending on application state, use when to conditionally skip the step entirely:

{
  target: '#admin-panel',
  title: 'Admin Panel',
  content: 'Manage users and settings.',
  // Only show this step if the admin panel exists
  when: () => document.querySelector('#admin-panel') !== null,
}

This is useful when the element's existence depends on user permissions or feature flags, not just loading timing.

Using React refs for dynamic content

React refs can identify targets without a selector. The player still waits for the ref to point to a connected, visible element with a measurable box:

import { useSpotlightTarget, SpotlightTour } from 'react-tourlight'

function LazyPanel() {
  const panelRef = useSpotlightTarget()

  return (
    <>
      <SpotlightTour
        id="panel-tour"
        steps={[
          {
            target: panelRef,
            title: 'Panel',
            content: 'This panel loaded dynamically.',
          },
        ]}
      />
      <div ref={panelRef}>{/* dynamic content */}</div>
    </>
  )
}

Pass the same ref to the rendered target and the tour definition. They can live in different components if you pass or share that ref. CSS selectors let you keep the definition independent of React ref wiring.

Error handling

When a target element isn't found within the timeout:

  1. The unavailable step advances to the next step.
  2. A target timeout by itself does not throw a hook error.
  3. If no steps remain, the guide can reach its completed state.

Tour state tracks guide progression; it does not prove that every target was visible or that a business task succeeded. Use target diagnostics and browser checks to catch missing required elements before release.

An exception from application lifecycle code is different: the guide stops and reports the error through the provider's onError(error, tourId) callback. Make preparation hooks handle their expected application failures, and test cancellation while an asynchronous hook is pending.

Debugging missing elements

If steps are unexpectedly being skipped, check:

  • The CSS selector matches the element exactly (use document.querySelector() in DevTools)
  • The element is in the DOM, not inside a Shadow DOM
  • The element is visible and has nonzero dimensions; an existing but hidden element is not ready
  • If using a ref, its target mounts and becomes visible within the configured timeout