# Tour

URL: https://ark-ui.com/docs/components/tour
Source: https://raw.githubusercontent.com/chakra-ui/ark/refs/heads/main/website/src/content/pages/components/tour.mdx

A guided tour that helps users understand the interface.

---

<ComponentPreview id="Tour" />

## Anatomy

<Anatomy id="tour" />

```tsx
const tour = useTour({ steps: [...] })

<Tour.Root tour={tour}>
  <Tour.Backdrop />
  <Tour.Spotlight />
  <Tour.Positioner>
    <Tour.Content>
      <Tour.Arrow>
        <Tour.ArrowTip />
      </Tour.Arrow>
      <Tour.Title />
      <Tour.Description />
      <Tour.ProgressText />
      <Tour.CloseTrigger />
      <Tour.Actions>
        <Tour.ActionTrigger />
      </Tour.Actions>
    </Tour.Content>
  </Tour.Positioner>
</Tour.Root>
```

## Examples

**Example: basic**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Ellipsis, Save, Sparkles, Upload, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'welcome',
    type: 'dialog',
    title: 'Welcome to the App!',
    description: 'Let\'s take a quick tour to get you started with the main features.',
    actions: [
      { label: 'Start Tour', action: 'next' },
    ],
  },
  {
    id: 'upload',
    type: 'tooltip',
    title: 'Upload Files',
    description: 'Click here to upload your files to the cloud.',
    target: () => document.querySelector<HTMLElement>('#btn-upload'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'save',
    type: 'tooltip',
    title: 'Save Changes',
    description: 'Save your work to keep your progress.',
    target: () => document.querySelector<HTMLElement>('#btn-save'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'more',
    type: 'tooltip',
    title: 'More Options',
    description: 'Access additional settings and actions from this menu.',
    target: () => document.querySelector<HTMLElement>('#btn-more'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'complete',
    type: 'dialog',
    title: 'You\'re all set!',
    description: 'You now know the basics. Enjoy using the app!',
    actions: [
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component Basic() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div class={styles.ActionButtons}>
      <button id="btn-upload" type="button" class={button.Root}>
        <Upload />
        {'Upload'}
      </button>
      <button id="btn-save" type="button" class={button.Root}>
        <Save />
        {'Save'}
      </button>
      <button id="btn-more" type="button" class={button.Root}>
        <Ellipsis />
        {'More'}
      </button>
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Step Types

Demonstrate all three step types in a single tour: `dialog` for welcome/completion, `tooltip` anchored to elements, and
`floating` for fixed-position content.

**Example: mixed-types**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'welcome',
    type: 'dialog',
    title: 'Welcome!',
    description: 'This tour demonstrates different step types: dialog, tooltip, and floating.',
    actions: [
      { label: 'Start Tour', action: 'next' },
    ],
  },
  {
    id: 'tooltip-step',
    type: 'tooltip',
    title: 'Tooltip Step',
    description: 'This step appears as a tooltip anchored to a specific element.',
    target: () => document.querySelector<HTMLElement>('#target-element'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'floating-step',
    type: 'floating',
    placement: 'bottom-end',
    title: 'Floating Step',
    description: 'This step floats at a fixed position on the screen, independent of any target.',
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'complete',
    type: 'dialog',
    title: 'Tour Complete!',
    description: 'You have seen all the different step types available.',
    actions: [
      { label: 'Done', action: 'dismiss' },
    ],
  },
];

export component MixedTypes() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div id="target-element" class={styles.Target}>{'Target Element'}</div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Progress

Display a visual progress indicator at the bottom of the tour content showing how far along the user is.

**Example: progress-bar**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    title: 'Progress Tracking',
    description: 'Watch the progress bar at the bottom as you navigate.',
    target: () => document.querySelector<HTMLElement>('#progress-1'),
    actions: [
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-2',
    type: 'tooltip',
    title: 'Halfway There',
    description: 'The progress bar shows how far along you are.',
    target: () => document.querySelector<HTMLElement>('#progress-2'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-3',
    type: 'tooltip',
    title: 'Almost Done',
    description: 'One more step to complete the tour.',
    target: () => document.querySelector<HTMLElement>('#progress-3'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-4',
    type: 'tooltip',
    title: 'Complete!',
    description: 'You have completed all the steps.',
    target: () => document.querySelector<HTMLElement>('#progress-4'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component ProgressBar() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div class={styles.ActionButtons}>
      <div id="progress-1" class={styles.Target}>{'Step 1'}</div>
      <div id="progress-2" class={styles.Target}>{'Step 2'}</div>
      <div id="progress-3" class={styles.Target}>{'Step 3'}</div>
      <div id="progress-4" class={styles.Target}>{'Step 4'}</div>
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
            <div class={styles.ProgressBarBottom}>
              <div
                class={styles.ProgressFill}
                style={{ width: `${@tour.getProgressPercent()}%` }}
              />
            </div>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Skip

Allow users to skip the entire tour at any step by adding a skip action.

**Example: skip-tour**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    title: 'First Feature',
    description: 'You can skip this tour at any time using the Skip button.',
    target: () => document.querySelector<HTMLElement>('#item-1'),
    actions: [
      { label: 'Skip', action: 'dismiss' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-2',
    type: 'tooltip',
    title: 'Second Feature',
    description: 'Continue or skip to end the tour early.',
    target: () => document.querySelector<HTMLElement>('#item-2'),
    actions: [
      { label: 'Skip', action: 'dismiss' },
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-3',
    type: 'tooltip',
    title: 'Final Feature',
    description: 'This is the last step of the tour.',
    target: () => document.querySelector<HTMLElement>('#item-3'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component SkipTour() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div class={styles.ActionButtons}>
      <div id="item-1" class={styles.Target}>{'Item 1'}</div>
      <div id="item-2" class={styles.Target}>{'Item 2'}</div>
      <div id="item-3" class={styles.Target}>{'Item 3'}</div>
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Keyboard Navigation

Enable arrow key navigation between tour steps using the `keyboardNavigation` prop.

**Example: keyboard-navigation**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Keyboard, Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    title: 'Keyboard Navigation',
    description: 'Press the right arrow key (→) to go to the next step.',
    target: () => document.querySelector<HTMLElement>('#key-1'),
    actions: [
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-2',
    type: 'tooltip',
    title: 'Go Back',
    description: 'Press the left arrow key (←) to go back.',
    target: () => document.querySelector<HTMLElement>('#key-2'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-3',
    type: 'tooltip',
    title: 'Close Tour',
    description: 'Press Escape to close the tour at any time.',
    target: () => document.querySelector<HTMLElement>('#key-3'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component KeyboardNavigation() {
  const tour = useTour({ steps, keyboardNavigation: true });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <p class={styles.Hint}>
      <Keyboard />
      {'Use arrow keys to navigate, Escape to close'}
    </p>

    <div class={styles.ActionButtons}>
      <div id="key-1" class={styles.Target}>{'Step 1'}</div>
      <div id="key-2" class={styles.Target}>{'Step 2'}</div>
      <div id="key-3" class={styles.Target}>{'Step 3'}</div>
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Events

Listen to tour lifecycle events like `onStepChange` and `onStatusChange` to track user progress.

**Example: events**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { track } from 'ripple';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    title: 'First Step',
    description: 'Watch the event log below as you navigate.',
    target: () => document.querySelector<HTMLElement>('#event-1'),
    actions: [
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-2',
    type: 'tooltip',
    title: 'Second Step',
    description: 'Each step change triggers an event.',
    target: () => document.querySelector<HTMLElement>('#event-2'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'step-3',
    type: 'tooltip',
    title: 'Final Step',
    description: 'Complete the tour to see the status change.',
    target: () => document.querySelector<HTMLElement>('#event-3'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component Events() {
  let logs = track<string[]>([]);

  const tour = useTour(
    {
      steps,
      onStepChange(details) {
        @logs = [...@logs, `Step changed: ${details.stepId}`];
      },
      onStatusChange(details) {
        @logs = [...@logs, `Status: ${details.status}`];
      },
    },
  );

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div class={styles.ActionButtons}>
      <div id="event-1" class={styles.Target}>{'Step 1'}</div>
      <div id="event-2" class={styles.Target}>{'Step 2'}</div>
      <div id="event-3" class={styles.Target}>{'Step 3'}</div>
    </div>

    <div class={styles.EventLog}>
      <strong>{'Event Log:'}</strong>
      if (@logs.length === 0) {
        <div class={styles.EventLogItem}>{'Start the tour to see events'}</div>
      } else {
        for (const log of @logs; key log) {
          <div class={styles.EventLogItem}>{log}</div>
        }
      }
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Wait for Click

Use the `effect` function with `waitForEvent` to wait for user interaction before proceeding to the next step.

**Example: wait-for-click**

```ripple
import { Tour, useTour, waitForEvent } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'intro',
    type: 'dialog',
    title: 'Interactive Tutorial',
    description: 'This tour will guide you through actions. You must complete each step to proceed.',
    actions: [
      { label: 'Begin', action: 'next' },
    ],
  },
  {
    id: 'click-add',
    type: 'tooltip',
    title: 'Click the Add Button',
    description: 'Click the "Add Item" button to continue.',
    target: () => document.querySelector<HTMLElement>('#btn-add'),
    effect({ next, target, show }) {
      show();
      const [promise, cancel] = waitForEvent(target, 'click');
      promise.then(() => next());
      return cancel;
    },
  },
  {
    id: 'click-edit',
    type: 'tooltip',
    title: 'Click the Edit Button',
    description: 'Now click the "Edit" button.',
    target: () => document.querySelector<HTMLElement>('#btn-edit'),
    effect({ next, target, show }) {
      show();
      const [promise, cancel] = waitForEvent(target, 'click');
      promise.then(() => next());
      return cancel;
    },
  },
  {
    id: 'click-delete',
    type: 'tooltip',
    title: 'Click the Delete Button',
    description: 'Finally, click the "Delete" button.',
    target: () => document.querySelector<HTMLElement>('#btn-delete'),
    effect({ next, target, show }) {
      show();
      const [promise, cancel] = waitForEvent(target, 'click');
      promise.then(() => next());
      return cancel;
    },
  },
  {
    id: 'complete',
    type: 'dialog',
    title: 'Well Done!',
    description: 'You completed all the interactive steps.',
    actions: [
      { label: 'Finish', action: 'dismiss' },
    ],
  },
];

export component WaitForClick() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Interactive Tour'}
    </button>

    <div class={styles.ActionButtons}>
      <button id="btn-add" type="button" class={button.Root}>{'Add Item'}</button>
      <button id="btn-edit" type="button" class={button.Root}>{'Edit'}</button>
      <button id="btn-delete" type="button" class={button.Root}>{'Delete'}</button>
    </div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

### Wait for Input

Create form tutorials that wait for users to enter valid input before advancing.

<Example id="wait-for-input" />

### Wait for Element

Wait for dynamically rendered elements to appear in the DOM before showing a step.

<Example id="wait-for-element" />

### Async

Load data asynchronously and update step content before displaying it using the `effect` function with `show()` and
`update()`.

**Example: async-step**

```ripple
import { Tour, useTour } from 'ark-ripple/tour';
import { Portal } from 'ark-ripple/portal';
import { Sparkles, X } from 'lucide-ripple';
import button from 'styles/button.module.css';
import styles from 'styles/tour.module.css';

const steps: Tour.StepDetails[] = [
  {
    id: 'intro',
    type: 'dialog',
    title: 'Async Data Loading',
    description: 'This tour demonstrates loading data before showing a step.',
    actions: [
      { label: 'Next', action: 'next' },
    ],
  },
  {
    id: 'user-info',
    type: 'tooltip',
    title: 'Loading...',
    description: 'Fetching user data...',
    target: () => document.querySelector<HTMLElement>('#user-card'),
    actions: [
      { label: 'Back', action: 'prev' },
      { label: 'Next', action: 'next' },
    ],
    effect({ show, update }) {
      const controller = new AbortController();

      fetch('https://api.github.com/users/segunadebayo', { signal: controller.signal }).then(
        (res) => res.json(),
      ).then((data) => {
        update(
          {
            title: `Welcome, ${data.name || data.login}!`,
            description: `You have ${data.public_repos} public repositories and ${data.followers} followers.`,
          },
        );
        show();
      }).catch(() => {
        update(
          {
            title: 'User Profile',
            description: 'Could not load user data. Please try again.',
          },
        );
        show();
      });

      return () => controller.abort();
    },
  },
  {
    id: 'complete',
    type: 'dialog',
    title: 'Tour Complete',
    description: 'The async step loaded data from the GitHub API before displaying.',
    actions: [
      { label: 'Done', action: 'dismiss' },
    ],
  },
];

export component AsyncStep() {
  const tour = useTour({ steps });

  <div class={styles.Root}>
    <button
      type="button"
      data-variant="surface"
      class={button.Root}
      onClick={() => {
        @tour.start();
      }}
    >
      <Sparkles />
      {'Start Tour'}
    </button>

    <div id="user-card" class={styles.Target}>{'User Profile Card'}</div>

    <Tour.Root {tour}>
      <Portal>
        <Tour.Backdrop class={styles.Backdrop} />
        <Tour.Spotlight class={styles.Spotlight} />
        <Tour.Positioner class={styles.Positioner}>
          <Tour.Content class={styles.Content}>
            <Tour.Arrow class={styles.Arrow}>
              <Tour.ArrowTip class={styles.ArrowTip} />
            </Tour.Arrow>
            <Tour.CloseTrigger class={styles.CloseTrigger}>
              <X />
            </Tour.CloseTrigger>
            <Tour.ProgressText class={styles.ProgressText} />
            <Tour.Title class={styles.Title} />
            <Tour.Description class={styles.Description} />
            <Tour.Control class={styles.Control}>
              <Tour.Actions>
                component children({ actions }) {
                  for (const action of @actions; key action.label) {
                    <Tour.ActionTrigger class={styles.ActionTrigger} {action} />
                  }
                }
              </Tour.Actions>
            </Tour.Control>
          </Tour.Content>
        </Tour.Positioner>
      </Portal>
    </Tour.Root>
  </div>
}

```

## Guides

### Step Types

The tour machine supports different types of steps, allowing you to create a diverse and interactive tour experience.
The available step types are defined in the `StepType` type:

- `tooltip`: Displays the step content as a tooltip, typically positioned near the target element.

- `dialog`: Shows the step content in a modal dialog centered on screen, useful for starting or ending the tour. This
  usually don't have a `target` defined.

- `floating`: Presents the step content as a floating element, which can be positioned flexibly on the screen. This
  usually don't have a `target` defined.

- `wait`: A special type that waits for a specific condition before proceeding to the next step.

```tsx
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    placement: 'top-start',
    target: () => document.querySelector('#target-1'),
    title: 'Tooltip Step',
    description: 'This is a tooltip step',
  },
  {
    id: 'step-2',
    type: 'dialog',
    title: 'Dialog Step',
    description: 'This is a dialog step',
  },
  {
    id: 'step-3',
    type: 'floating',
    placement: 'top-start',
    title: 'Floating Step',
    description: 'This is a floating step',
  },
  {
    id: 'step-4',
    type: 'wait',
    title: 'Wait Step',
    description: 'This is a wait step',
    effect({ next }) {
      // do something and go next
      // you can also return a cleanup
    },
  },
]
```

### Actions

Every step supports a list of actions that are rendered in the step footer.Use the `actions` property to define each
action.

```tsx
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'dialog',
    title: 'Dialog Step',
    description: 'This is a dialog step',
    actions: [{ label: 'Show me a tour!', action: 'next' }],
  },
]
```

### Tooltip Placement

Use the `placement` property to define the placement of the tooltip.

```tsx {5}
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    placement: 'top-start',
    // ...
  },
]
```

### Hide Arrow

Set `arrow: false` in the step property to hide the tooltip arrow. This is only useful for tooltip steps.

```tsx {5}
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    arrow: false,
  },
]
```

### Hide Backdrop

Set `backdrop: false` in the step property to hide the backdrop. This applies to all step types except the `wait` step.

```tsx {5}
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'dialog',
    backdrop: false,
  },
]
```

### Effects

Step effects are functions that are called before a step is opened. They are useful for adding custom logic to a step.

This function provides the following methods:

- `next()`: Call this method to move to the next step.
- `show()`: Call this method to show the current step.
- `update(details: StepDetails)`: Call this method to update the details of the current step (say, after data has been
  fetched).

```tsx
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'tooltip',
    effect({ next, show, update }) {
      fetchData().then((res) => {
        // update the step details
        update({ title: res.title })
        // then show show the step
        show()
      })

      return () => {
        // cleanup fetch data
      }
    },
  },
]
```

### Wait

Wait steps are useful when you need to wait for a specific condition before proceeding to the next step.

Use the step `effect` function to perform an action and then call `next()` to move to the next step.

> **Note:** You cannot call `show()` in a wait step.

```tsx
const steps: TourStepDetails[] = [
  {
    id: 'step-1',
    type: 'wait',
    effect({ next }) {
      const button = document.querySelector('#button')
      const listener = () => next()
      button.addEventListener('click', listener)
      return () => button.removeEventListener('click', listener)
    },
  },
]
```

### Styling

Ensure the `box-sizing` is set to `border-box` for the means of measuring the tour target.

```css
* {
  box-sizing: border-box;
}
```

Ensure the `body` has a `position` of `relative`.

```css
body {
  position: relative;
}
```

## API Reference

### Props

**Component API Reference**

**Root Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `tour` | `UseTourReturn` | Yes |  |
| `immediate` | `boolean` | No | Whether to synchronize the present change immediately or defer it to the next frame |
| `lazyMount` | `boolean` | No | Whether to enable lazy mounting |
| `onExitComplete` | `VoidFunction` | No | Function called when the animation ends in the closed state |
| `present` | `boolean` | No | Whether the node is present (controlled by the user) |
| `skipAnimationOnMount` | `boolean` | No | Whether to allow the initial presence animation. |
| `unmountOnExit` | `boolean` | No | Whether to unmount on exit. |


**ActionTrigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `action` | `StepAction` | Yes |  |
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**ActionTrigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | action-trigger |
| `[data-type]` | The type of the item |
| `[data-disabled]` | Present when disabled |


**Arrow Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Arrow CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--arrow-size` | The size of the arrow |
| `--arrow-size-half` | Half the size of the arrow |
| `--arrow-background` | Use this variable to style the arrow background |
| `--arrow-offset` | The offset position of the arrow |


**ArrowTip Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Backdrop Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Backdrop Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | backdrop |
| `[data-state]` | "open" | "closed" |
| `[data-type]` | The type of the item |


**Backdrop CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--tour-layer` | The tour layer value for the Backdrop |
| `--layer-index` | The index of the dismissable in the layer stack |


**CloseTrigger Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**CloseTrigger Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | close-trigger |
| `[data-type]` | The type of the item |


**Content Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Content Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | content |
| `[data-state]` | "open" | "closed" |
| `[data-nested]` | popover |
| `[data-has-nested]` | popover |
| `[data-type]` | The type of the item |
| `[data-placement]` | The placement of the content |
| `[data-step]` |  |


**Content CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--layer-index` | The index of the dismissable in the layer stack |
| `--nested-layer-count` | The number of nested tours |


**Control Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Description Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Description Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | description |
| `[data-placement]` | The placement of the description |


**Positioner Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Positioner Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | positioner |
| `[data-type]` | The type of the item |
| `[data-placement]` | The placement of the positioner |


**Positioner CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--tour-layer` | The tour layer value for the Positioner |
| `--reference-width` | The width of the reference element |
| `--reference-height` | The height of the root |
| `--available-width` | The available width in viewport |
| `--available-height` | The available height in viewport |
| `--x` | The x position for transform |
| `--y` | The y position for transform |
| `--z-index` | The z-index value |
| `--transform-origin` | The transform origin for animations |


**ProgressText Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Spotlight Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Spotlight CSS Variables:**

| Variable | Description |
|----------|-------------|
| `--tour-layer` | The tour layer value for the Spotlight |


**Title Props:**

| Prop | Type | Required | Description |
|------|------|----------|-------------|
| `asChild` | `boolean` | No | Use the provided child element as the default rendered element, combining their props and behavior. |


**Title Data Attributes:**

| Attribute | Value |
|-----------|-------|
| `[data-scope]` | tour |
| `[data-part]` | title |
| `[data-placement]` | The placement of the title |


### Context

**API:**

| Property | Type | Description |
|----------|------|-------------|
| `open` | `boolean` | Whether the tour is open |
| `totalSteps` | `number` | The total number of steps |
| `stepIndex` | `number` | The index of the current step |
| `step` | `StepDetails` | The current step details |
| `hasNextStep` | `boolean` | Whether there is a next step |
| `hasPrevStep` | `boolean` | Whether there is a previous step |
| `firstStep` | `boolean` | Whether the current step is the first step |
| `lastStep` | `boolean` | Whether the current step is the last step |
| `addStep` | `(step: StepDetails) => void` | Add a new step to the tour |
| `removeStep` | `(id: string) => void` | Remove a step from the tour |
| `updateStep` | `(id: string, stepOverrides: Partial<StepDetails>) => void` | Update a step in the tour with partial details |
| `setSteps` | `(steps: StepDetails[]) => void` | Set the steps of the tour |
| `setStep` | `(id: string) => void` | Set the current step of the tour |
| `start` | `(id?: string) => void` | Start the tour at a specific step (or the first step if not provided) |
| `isValidStep` | `(id: string) => boolean` | Check if a step is valid |
| `isCurrentStep` | `(id: string) => boolean` | Check if a step is visible |
| `next` | `VoidFunction` | Move to the next step |
| `prev` | `VoidFunction` | Move to the previous step |
| `getProgressText` | `() => string` | Returns the progress text |
| `getProgressPercent` | `() => number` | Returns the progress percent |
