Tour

Guide users through your application with interactive step-by-step tours

Usage

Tour is a step-by-step guide for application onboarding. It highlights target elements one by one and displays a tooltip with navigation controls next to each of them. Tour.Step components must be direct children of Tour.

import { useState } from 'react';
import { Button, Group, TextInput, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Group>
        <Button id="tour-target-1">First target</Button>
        <TextInput id="tour-target-2" placeholder="Second target" />
        <Button id="tour-target-3" variant="outline">
          Third target
        </Button>
      </Group>

      <Button mt="md" onClick={() => { setStep(0); setActive(true); }}>
        Start tour
      </Button>

      <Tour active={active} step={step} onStepChange={setStep} onClose={() => setActive(false)}>
        <Tour.Step target="#tour-target-1" title="Step 1">
          This is the first step of the tour.
        </Tour.Step>
        <Tour.Step target="#tour-target-2" title="Step 2">
          This is the second step of the tour.
        </Tour.Step>
        <Tour.Step target="#tour-target-3" title="Step 3">
          This is the third and final step.
        </Tour.Step>
      </Tour>
    </>
  );
}

Complex example

A dashboard onboarding tour: it starts with a centered welcome step, scrolls to targets inside ScrollArea and changes position and spotlightPadding of individual steps.

JD

John Doe

Admin

Recent activity

12 new
UserActionStatus
AliceCreated project
Complete
BobUpdated settings
In progress
CarolDeleted file
Failed
DaveUploaded report
Complete
EveReviewed PR
Pending

Preferences

import { useState } from 'react';
import {
  ActionIcon,
  Avatar,
  Badge,
  Box,
  Button,
  Card,
  Group,
  Indicator,
  ScrollArea,
  Stack,
  Switch,
  Table,
  Tabs,
  Text,
  TextInput,
  Tour,
} from '@mantine/core';
import { Bell, GearSix, MagnifyingGlass } from '@phosphor-icons/react';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Card withBorder>
        <Group justify="space-between" mb="md">
          <Group>
            <Avatar id="complex-avatar" color="blue" radius="xl">
              JD
            </Avatar>
            <div>
              <Text size="sm" fw={500}>
                John Doe
              </Text>
              <Text size="xs" c="dimmed">
                Admin
              </Text>
            </div>
          </Group>

          <Group gap="xs">
            <TextInput
              id="complex-search"
              placeholder="Search..."
              size="xs"
              leftSection={<MagnifyingGlass size={14} />}
              w={200}
            />
            <Indicator id="complex-notifications" processing>
              <ActionIcon variant="default" size="lg">
                <Bell size={18} />
              </ActionIcon>
            </Indicator>
            <ActionIcon id="complex-settings" variant="default" size="lg">
              <GearSix size={18} />
            </ActionIcon>
          </Group>
        </Group>

        <Tabs id="complex-tabs" defaultValue="overview" mb="md">
          <Tabs.List>
            <Tabs.Tab value="overview">Overview</Tabs.Tab>
            <Tabs.Tab value="analytics">Analytics</Tabs.Tab>
            <Tabs.Tab value="reports">Reports</Tabs.Tab>
            <Tabs.Tab value="settings">Settings</Tabs.Tab>
          </Tabs.List>
        </Tabs>

        <ScrollArea h={200} type="always">
          <Stack gap="md">
            <Group justify="space-between">
              <Text fw={500}>Recent activity</Text>
              <Badge id="complex-badge" variant="light" color="green">
                12 new
              </Badge>
            </Group>

            <Table id="complex-table">
              <Table.Thead>
                <Table.Tr>
                  <Table.Th>User</Table.Th>
                  <Table.Th>Action</Table.Th>
                  <Table.Th>Status</Table.Th>
                </Table.Tr>
              </Table.Thead>
              <Table.Tbody>
                <Table.Tr>
                  <Table.Td>Alice</Table.Td>
                  <Table.Td>Created project</Table.Td>
                  <Table.Td>
                    <Badge color="green" variant="light" size="sm">
                      Complete
                    </Badge>
                  </Table.Td>
                </Table.Tr>
                <Table.Tr>
                  <Table.Td>Bob</Table.Td>
                  <Table.Td>Updated settings</Table.Td>
                  <Table.Td>
                    <Badge color="blue" variant="light" size="sm">
                      In progress
                    </Badge>
                  </Table.Td>
                </Table.Tr>
                <Table.Tr>
                  <Table.Td>Carol</Table.Td>
                  <Table.Td>Deleted file</Table.Td>
                  <Table.Td>
                    <Badge color="red" variant="light" size="sm">
                      Failed
                    </Badge>
                  </Table.Td>
                </Table.Tr>
                <Table.Tr>
                  <Table.Td>Dave</Table.Td>
                  <Table.Td>Uploaded report</Table.Td>
                  <Table.Td>
                    <Badge color="green" variant="light" size="sm">
                      Complete
                    </Badge>
                  </Table.Td>
                </Table.Tr>
                <Table.Tr>
                  <Table.Td>Eve</Table.Td>
                  <Table.Td>Reviewed PR</Table.Td>
                  <Table.Td>
                    <Badge color="yellow" variant="light" size="sm">
                      Pending
                    </Badge>
                  </Table.Td>
                </Table.Tr>
              </Table.Tbody>
            </Table>

            <Box
              id="complex-preferences"
              p="md"
              style={{
                border: '1px solid var(--mantine-color-default-border)',
                borderRadius: 'var(--mantine-radius-default)',
              }}
            >
              <Text fw={500} mb="xs">
                Preferences
              </Text>
              <Stack gap="xs">
                <Switch label="Email notifications" defaultChecked />
                <Switch label="Push notifications" />
                <Switch label="Weekly digest" defaultChecked />
              </Stack>
            </Box>
          </Stack>
        </ScrollArea>
      </Card>

      <Button mt="md" onClick={() => { setStep(0); setActive(true); }}>
        Start tour
      </Button>

      <Tour
        active={active}
        step={step}
        onStepChange={setStep}
        onClose={() => setActive(false)}
        closeOnOverlayClick
      >
        <Tour.Step title="Welcome to the dashboard">
          Let us show you around! This tour will walk you through the main features
          of your new dashboard.
        </Tour.Step>

        <Tour.Step target="#complex-avatar" title="Your profile" position="bottom-start">
          This is your profile section. Click on your avatar to manage account settings,
          change your role, or sign out.
        </Tour.Step>

        <Tour.Step target="#complex-search" title="Search" position="bottom">
          Use the search bar to quickly find projects, users, reports, and settings
          across the entire application.
        </Tour.Step>

        <Tour.Step target="#complex-notifications" title="Notifications" position="bottom-end">
          The notification bell shows real-time alerts. The pulsing indicator means
          you have unread notifications.
        </Tour.Step>

        <Tour.Step target="#complex-tabs" title="Navigation tabs" position="bottom">
          Switch between different sections of your dashboard using these tabs.
          Each tab provides a different view of your data.
        </Tour.Step>

        <Tour.Step
          target="#complex-table"
          title="Activity feed"
          position="top"
          spotlightPadding={16}
        >
          The activity table shows recent actions by your team members.
          Each row displays the user, their action, and the current status.
          This element is inside a scroll area – the tour scrolls it into view automatically.
        </Tour.Step>

        <Tour.Step
          target="#complex-preferences"
          title="Preferences"
          position="top"
          spotlightPadding={0}
        >
          Scroll down to find your notification preferences. Customize
          which alerts you receive and how often. You are all set!
        </Tour.Step>
      </Tour>
    </>
  );
}

Beacon mode

Set mode="beacon" to display pulsing beacons next to all targets instead of a sequential walkthrough. Users can click beacons in any order to open tooltips. Beacons stay on the page until active is set to false.

import { useState } from 'react';
import { Button, Group, TextInput, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);

  return (
    <>
      <Group>
        <Button id="beacon-target-1">First target</Button>
        <TextInput id="beacon-target-2" placeholder="Second target" />
        <Button id="beacon-target-3" variant="outline">
          Third target
        </Button>
      </Group>

      <Button mt="md" onClick={() => setActive((current) => !current)}>
        {active ? 'End beacon tour' : 'Start beacon tour'}
      </Button>

      <Tour active={active} mode="beacon" defaultStep={-1} color="teal">
        <Tour.Step target="#beacon-target-1" title="Beacon 1">
          Click the beacon to see this tooltip.
        </Tour.Step>
        <Tour.Step target="#beacon-target-2" title="Beacon 2">
          This is the second beacon tooltip.
        </Tour.Step>
        <Tour.Step target="#beacon-target-3" title="Beacon 3">
          This is the third beacon tooltip.
        </Tour.Step>
      </Tour>
    </>
  );
}

Controlled

Use step and onStepChange props to control the current step. An uncontrolled tour always starts from defaultStep.

import { useState } from 'react';
import { Button, Group, TextInput, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Group>
        <Button id="controlled-target-1">First target</Button>
        <TextInput id="controlled-target-2" placeholder="Second target" />
      </Group>

      <Group mt="md">
        <Button onClick={() => { setStep(0); setActive(true); }}>Go to step 1</Button>
        <Button onClick={() => { setStep(1); setActive(true); }}>Go to step 2</Button>
      </Group>

      <Tour active={active} step={step} onStepChange={setStep} onClose={() => setActive(false)}>
        <Tour.Step target="#controlled-target-1" title="Step 1">
          This is the first step.
        </Tour.Step>
        <Tour.Step target="#controlled-target-2" title="Step 2">
          This is the second step.
        </Tour.Step>
      </Tour>
    </>
  );
}

Overlay configuration

  • withOverlay – display the backdrop overlay, true by default
  • withOverlayInteraction – allow users to interact with the highlighted element, false by default
  • closeOnOverlayClick – close the tour when the overlay is clicked, false by default

Dropdowns rendered in a portal (Select, Menu, etc.) are displayed below the tour. To use them with withOverlayInteraction, set their zIndex to a value greater than the tour zIndex, for example comboboxProps={{ zIndex: 10001 }}.

import { useState } from 'react';
import { Button, Group, Tour } from '@mantine/core';
function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Group>
        <Button id="overlay-target-1">First target</Button>
        <Button id="overlay-target-2" variant="light">Second target</Button>
      </Group>

      <Button mt="md" onClick={() => { setStep(0); setActive(true); }}>
        Start tour
      </Button>

      <Tour
        active={active}
        step={step}
        onStepChange={setStep}
        onClose={() => setActive(false)}

      >
        <Tour.Step target="#overlay-target-1" title="First step">
          This is the first step of the tour.
        </Tour.Step>
        <Tour.Step target="#overlay-target-2" title="Second step">
          This is the second step of the tour.
        </Tour.Step>
      </Tour>
    </>
  );
}

Step targets

target prop of Tour.Step accepts a CSS selector or a ref. Use a selector for elements that are mounted after the step is activated, a ref must point to an element that is already mounted.

Steps without target

Steps without target are displayed in the center of the screen. Use them for welcome and completion screens.

import { useState } from 'react';
import { Button, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Button id="centered-target" onClick={() => { setStep(0); setActive(true); }}>
        Start tour
      </Button>

      <Tour active={active} step={step} onStepChange={setStep} onClose={() => setActive(false)}>
        <Tour.Step title="Welcome">
          This step has no target and is displayed in the center of the screen.
        </Tour.Step>
        <Tour.Step target="#centered-target" title="Target step">
          This step highlights the button.
        </Tour.Step>
        <Tour.Step title="All done">
          This final step is also centered with no target.
        </Tour.Step>
      </Tour>
    </>
  );
}

Custom content

Tour.Step children can be any React node. To build the tooltip layout yourself, use compound components. Compound components do not know your steps:

  • Pass the total number of steps to Tour.Root with stepsCount prop
  • Find target elements yourself and pass them to Tour.Overlay (targetRect) and Tour.Tooltip (targetElement)
  • Set a distinct aria-label on each Tour.Beacon
  • To play the exit transition, set mounted={false} on Tour.Tooltip first and active={false} on Tour.Root after the transition ends
import { useEffect, useState } from 'react';
import { Button, Group, Tour } from '@mantine/core';

const steps = [
  { target: '#compound-1', title: 'Welcome', body: 'Click here to get started with the app.', position: 'bottom' as const },
  { target: '#compound-2', title: 'Settings', body: 'Configure your preferences here.', position: 'bottom' as const },
];

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);
  const [targetElement, setTargetElement] = useState<HTMLElement | null>(null);
  const [targetRect, setTargetRect] = useState<DOMRect | null>(null);

  const currentStep = steps[step];

  useEffect(() => {
    const el = active ? document.querySelector<HTMLElement>(currentStep.target) : null;
    setTargetElement(el);

    if (!el) {
      setTargetRect(null);
      return undefined;
    }

    const updateRect = () => setTargetRect(el.getBoundingClientRect());
    updateRect();

    window.addEventListener('scroll', updateRect, true);
    window.addEventListener('resize', updateRect);

    return () => {
      window.removeEventListener('scroll', updateRect, true);
      window.removeEventListener('resize', updateRect);
    };
  }, [active, step]);

  return (
    <>
      <Group>
        <Button id="compound-1" onClick={() => { setActive(true); setStep(0); }}>
          Start tour
        </Button>
        <Button id="compound-2" variant="light">
          Settings
        </Button>
      </Group>

      <Tour.Root
        active={active}
        step={step}
        stepsCount={steps.length}
        onStepChange={setStep}
        onClose={() => setActive(false)}
      >
        <Tour.Overlay targetRect={targetRect} />
        <Tour.Tooltip
          targetElement={targetElement}
          position={currentStep?.position}
          mounted={active}
        >
          <Tour.CloseButton />
          <Tour.Title>{currentStep?.title}</Tour.Title>
          <Tour.Body>{currentStep?.body}</Tour.Body>
          <Tour.Navigation />
        </Tour.Tooltip>
      </Tour.Root>
    </>
  );
}

Tooltip position

Set position on Tour.Step to change the tooltip placement. If there is not enough space, the tooltip flips to the opposite side.

import { useState } from 'react';
import { Button, Center, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Center mih={360}>
        <Button id="position-target" onClick={() => { setStep(0); setActive(true); }}>
          Start tour
        </Button>
      </Center>

      <Tour active={active} step={step} onStepChange={setStep} onClose={() => setActive(false)}>
        <Tour.Step target="#position-target" title="Bottom" position="bottom">
          Tooltip positioned at the bottom.
        </Tour.Step>
        <Tour.Step target="#position-target" title="Top" position="top">
          Tooltip positioned at the top.
        </Tour.Step>
        <Tour.Step target="#position-target" title="Left" position="left">
          Tooltip positioned at the left.
        </Tour.Step>
        <Tour.Step target="#position-target" title="Right" position="right">
          Tooltip positioned at the right.
        </Tour.Step>
      </Tour>
    </>
  );
}

Custom labels

Use labels prop to translate navigation buttons:

import { useState } from 'react';
import { Button, Group, TextInput, Tour } from '@mantine/core';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Group>
        <Button id="labels-target-1">Primer objetivo</Button>
        <TextInput id="labels-target-2" placeholder="Segundo objetivo" />
      </Group>

      <Button mt="md" onClick={() => { setStep(0); setActive(true); }}>
        Iniciar recorrido
      </Button>

      <Tour
        active={active}
        step={step}
        onStepChange={setStep}
        onClose={() => setActive(false)}
        labels={{
          next: 'Siguiente',
          back: 'Anterior',
          skip: 'Omitir',
          close: 'Cerrar',
          stepCounter: (current, total) => `${current} de ${total}`,
        }}
      >
        <Tour.Step target="#labels-target-1" title="Paso 1">
          Este es el primer paso del recorrido.
        </Tour.Step>
        <Tour.Step target="#labels-target-2" title="Paso 2">
          Este es el segundo paso del recorrido.
        </Tour.Step>
      </Tour>
    </>
  );
}

Styles API

import { useState } from 'react';
import { Button, Group, TextInput, Tour } from '@mantine/core';
import classes from './Tour.demo.styles.module.css';

function Demo() {
  const [active, setActive] = useState(false);
  const [step, setStep] = useState(0);

  return (
    <>
      <Group>
        <Button id="styles-target-1">First target</Button>
        <TextInput id="styles-target-2" placeholder="Second target" />
        <Button id="styles-target-3" variant="outline">
          Third target
        </Button>
      </Group>

      <Button mt="md" onClick={() => { setStep(0); setActive(true); }}>
        Start tour
      </Button>

      <Tour
        active={active}
        step={step}
        onStepChange={setStep}
        onClose={() => setActive(false)}
        classNames={{
          tooltip: classes.tooltip,
          title: classes.title,
          body: classes.body,
          stepCounter: classes.stepCounter,
          closeButton: classes.closeButton,
          navigationButton: classes.navigationButton,
        }}
      >
        <Tour.Step target="#styles-target-1" title="Step 1: Welcome">
          This tour demonstrates how to customize Tour styles with classNames.
        </Tour.Step>
        <Tour.Step target="#styles-target-2" title="Step 2: Input">
          You can style every part of the tooltip independently.
        </Tour.Step>
        <Tour.Step target="#styles-target-3" title="Step 3: Actions">
          The tooltip, navigation buttons, and counters are all customized.
        </Tour.Step>
      </Tour>
    </>
  );
}

Keyboard navigation

Arrow keys are ignored while focus is inside the target element or an input. Set withKeyboardNavigation={false} to disable them.

KeyDescriptionCondition
ArrowRightGo to the next step, closes the tour on the last stepguided mode only
ArrowLeftGo to the previous stepguided mode only
EscapeClose the tourcloseOnEscape is true (default)

Accessibility

  • The tooltip has role="dialog", it is labelled by the step title and described by the step body
  • Focus is moved to the tooltip when it opens and is returned to the previously focused element when the tour closes
  • Focus is trapped inside the tooltip while the overlay is displayed
  • Step counter changes are announced with aria-live="polite"
  • Beacons are buttons labelled with labels.beacon and the step title, for example Start tour: Settings