APOLLO VISION LABS

Getting started

Install the two packages, run a first tour, and wire the provider to a real application.

guide walks a new user through an interface, one step at a time. The logic lives in one package and the Material UI rendering in another.

Two packages, and why

npm install @apollovisionlabs/guide-core @apollovisionlabs/guide-mui @mui/material @emotion/react @emotion/styled

@apollovisionlabs/guide-core holds the state machine, target resolution, route matching, persistence and the accessibility primitives. It renders nothing and has no runtime dependency beyond React.

@apollovisionlabs/guide-mui is one rendering of that engine: a spotlight overlay and a step popover, built on Material UI. Its only runtime dependency is the core.

If you draw your own popover, install the core alone and read the active step from useGuideStep(). Everything below assumes the Material UI layer, which is the shortest path to a running tour.

The smallest thing that works

import { GuideProvider, useTour, type Tour } from '@apollovisionlabs/guide-core';
import { GuideTour } from '@apollovisionlabs/guide-mui';

const welcomeTour: Tour = {
  id: 'welcome',
  steps: [
    {
      target: 'nav.projects',
      title: 'Your projects live here',
      body: 'Everything you create is grouped under a project.',
      placement: 'bottom',
    },
  ],
};

function StartButton() {
  const tour = useTour('welcome');
  return <button onClick={() => tour.start()}>Start the tour</button>;
}

export function App() {
  return (
    <GuideProvider tours={[welcomeTour]}>
      <nav>
        <a href="/projects" data-guide="nav.projects">
          Projects
        </a>
      </nav>
      <StartButton />
      <GuideTour />
    </GuideProvider>
  );
}

GuideProvider holds the state, GuideTour draws it. Both read the Material UI theme, so keep them inside your ThemeProvider if you have one.

Declare the tour at module scope, as above. A tour object rebuilt inside a component body is a new object on every render, and the missing target timeout is keyed on step object identity: recreate the steps each render and that timer never reaches its deadline. The symptom is a tour that hangs on a step forever, with onMissingTarget apparently ignored. If a tour has to be built at runtime, build it once in a useMemo with stable dependencies.

How a step finds its target

target is a logical key, not a CSS selector. The engine looks for [data-guide="<key>"], so the highlighted element declares its own participation:

<button data-guide="projects.create">New project</button>

A CSS selector couples the tour to markup that changes for unrelated reasons: a class renamed by a redesign, a wrapper added by a layout refactor, a generated class name from a styling library. A data-guide attribute is a contract, visible in the element’s own source, that a reviewer can see they are about to break.

Namespace the keys, as in nav.projects or projects.create. They appear in the step:show and target:missing events, so they end up in whatever you log.

The element does not have to exist yet. The engine watches the DOM with a MutationObserver for targetTimeoutMs (5000 by default), then applies the missing target policy described in Tours.

Wiring a real application

Five props turn the minimal example into something you can ship.

import { useLocation, useNavigate } from 'react-router';
import { useTranslation } from 'react-i18next';
import { GuideProvider, createBrowserStorage } from '@apollovisionlabs/guide-core';
import { GuideTour } from '@apollovisionlabs/guide-mui';
import { productTour } from './tours';

const storage = createBrowserStorage('my-app');

export function App() {
  const navigate = useNavigate();
  const location = useLocation();
  const { t } = useTranslation();

  return (
    <GuideProvider
      tours={[productTour]}
      navigate={(path) => navigate(path)}
      location={location.pathname}
      storage={storage}
      translate={(key) => t(key)}
      onEvent={(event) => console.info('[guide]', event)}
    >
      <AppRoutes />
      <GuideTour />
    </GuideProvider>
  );
}

navigate is called when a step lives on another page. location is the current pathname, which is how the engine knows whether it is already there. Omit location and every step is treated as being on the right page, so nothing ever navigates.

storage persists progress. Two implementations ship with the core: createMemoryStorage() for tests, createBrowserStorage(namespace) for localStorage. Neither talks to a server, and the packages make no network call of their own. On a workstation several people share, localStorage belongs to the browser profile and not to the person signed in, so the first person’s completed tour suppresses it for the second. Where accounts share a machine, implement GuideStorage against your own API.

translate resolves titleKey and bodyKey into text. It takes a key and returns a string, so any translation library fits in one line. Set a key with no translate and the raw key is displayed rather than a crash: that is the symptom to recognise. The popover’s own buttons are separate, and are overridden through labels on GuideTour:

<GuideTour
  labels={{
    next: t('common.next'),
    previous: t('common.back'),
    finish: t('common.finish'),
    close: t('common.close'),
  }}
/>

close is also the accessible name of the close button, so translating it is not cosmetic. Checklist and ChecklistLauncher take their own labels in the same way, covered in Checklist.

onEvent receives every GuideEvent: tour:start, tour:complete, tour:stop, step:show and target:missing. The library sends nothing anywhere; what you record and what consent it needs is your decision.

Peer requirements

React 19 for the core. React 19, Material UI 7 or 9, and Emotion 11 for the Material UI layer. Both packages ship a 'use client' banner on every emitted file, so importing them does not break a server build; GuideProvider still uses state and context, so the component rendering it is a client component. GuideTour returns null until it has mounted in the browser, which keeps the tour out of server rendered HTML.