Mantine Onboarding Tour

Undolog

@gfazioli/mantine-onboarding-tour

A Mantine component enables you to create a onboarding-tour effect using overlays, popovers, and onboarding tours, which enhances element visibility and interactivity.

Installation

yarn add @gfazioli/mantine-onboarding-tour

After installation import package styles at the root of your application:

import '@gfazioli/mantine-onboarding-tour/styles.css';

You can import styles within a layer @layer mantine-onboarding-tour by importing @gfazioli/mantine-onboarding-tour/styles.layer.css file.

import '@gfazioli/mantine-onboarding-tour/styles.layer.css';

Example

Here is a full page example of an Onboarding Tour.

Open Onboarding Tour example page

Usage

The OnboardingTour component allows to create onboarding experiences for your users.

A simple example of the Onboarding Tour component

👉 New amazing Mantine extension component

John Doe

Scroll the page 👆up or 👇down to remove the focus from the card. In practice, make this component invisible so that the onBlur event will be triggered.

Jane Doe

Scroll the page 👆up or 👇down to remove the focus from the card. In practice, make this component invisible so that the onBlur event will be triggered.

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Code, Divider, Group, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
function Demo() {
const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'welcome',
      title: 'Welcome to the Onboarding Tour Component',
      content:
        'This is a demo of the Onboarding Tour component, which allows to create onboarding experiences for your users.',
    },
    {
      id: 'subtitle',
      title: 'Subtitle',
      content: (
        <Text>
          You can select any component by using the <Code>data-onboarding-tour-id</Code> attribute
        </Text>
      ),
    },
    {
      id: 'button-see-all',
      title: 'New Features',
      content: 'Now you can click on the button "See all" to display all the testimonials',
    },
    {
      id: 'testimonial-2',
      title: 'New Testimonial Layout',
      content: 'We have improved the Testimonial layout',
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      maw={400}
      header={(tourController: OnboardingTourController) => (
        <Image
          mah={150}
          radius="md"
          src={`https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-${tourController.currentStepIndex + 1}.png`}
        />
      )}

    >
      <Stack justify="center" align="center">
        <Title data-onboarding-tour-id="welcome" order={4}>
          A simple example of the Onboarding Tour component
        </Title>
        <Text data-onboarding-tour-id="subtitle">👉 New amazing Mantine extension component</Text>

        <Center>
          <Button data-onboarding-tour-id="button-see-all">
            See all testimonials
          </Button>
        </Center>

        <Group justify="center">
          <Testimonials testimonial={0} />
          <Testimonials data-onboarding-tour-id="testimonial-2" testimonial={1} />
        </Group>
      </Stack>
    </OnboardingTour>
  );
}

OnboardingTourStep

The OnboardingTourStep interface defines the structure of each step in the tour.

export type OnboardingTourStep = {
  /** Unique id of the tour. Will be use for the data-onboarding-tour-id attribute */
  id: string;

  /** Header of the tour. You can also pass a React component here */
  header?:
    | React.ReactNode
    | ((tourController: OnboardingTourController) => React.ReactNode);

  /** Title of the tour. You can also pass a React component here */
  title?:
    | React.ReactNode
    | ((tourController: OnboardingTourController) => React.ReactNode);

  /** Custom Content of the tour. You can also pass a React component here */
  content?:
    | React.ReactNode
    | ((tourController: OnboardingTourController) => React.ReactNode);

  /** Footer of the tour. You can also pass a React component here */
  footer?:
    | React.ReactNode
    | ((tourController: OnboardingTourController) => React.ReactNode);

  /** Anything else */
  [key: string]: any;
};

Both header, title, content, and footer can be a string, a React component, or a function that receives tourController and returns a React component.

OnboardingTourController

The OnboardingTourController interface provides the current state of the tour. It also provides a series of actions to interact with the tour, such as the action to go to the next step nextStep() or to end the tour endTour().

export type OnboardingTourController = Readonly<{
  /** List of tour steps */
  tour: OnboardingTourStep[];

  /** Current step */
  currentStep: OnboardingTourStep | undefined;

  /** Current step index of the tour. Zero-based index */
  currentStepIndex: number | undefined;

  /** ID of the selected tour */
  selectedStepId: string | undefined;

  /** Set the current index */
  setCurrentStepIndex: (index: number) => void;

  /** Start the tour */
  startTour: () => void;

  /** End the tour */
  endTour: () => void;

  /** Go to the next tour */
  nextStep: () => void;

  /** Go to the previous tour */
  prevStep: () => void;

  /** Options of the tour */
  options: OnboardingTourOptions;
}>;

Define the onboarding tour

You can define your onboarding tour by using the OnboardingTourStep array.

const onboardingSteps: OnboardingTourStep[] = [
  {
    id: 'welcome',
    // Simple string
    title: 'Welcome to the Onboarding Tour Component',
    // Component
    content: <Text size="lg">Hello world!</Text>,
  },
  {
    id: 'subtitle',
    title: 'Another title',
    content: (tourController: OnboardingTourController) => (
      <Text size="lg">
        Hello world! {tourController.currentStep.id}
      </Text>
    ),
  },
];
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Code, Divider, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: (
        <Title order={4} c="lime">
          Title in a <Code>Title</Code> component
        </Title>
      ),
      content: (
        <Text c="red">
          Description in a <Code>Text</Code> component with color red
        </Text>
      ),
    },
    {
      id: 'step-2',
      title: 'Simple Title String',
      content: (tourController: OnboardingTourController) => (
        <Text>
          Description by using the function <Code>(tourController: OnboardingTourController)</Code> so we can get some more information such as the step: {tourController.currentStepIndex}
        </Text>
      ),
    },
    {
      id: 'step-3',
      content: <LoginForm />,
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Stack w={200} gap={32}>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Title
          </Button>
          <Button data-onboarding-tour-id="step-2" onClick={open}>
            Description
          </Button>
          <Button data-onboarding-tour-id="step-3" onClick={open}>
            Content
          </Button>
        </Stack>
      </Stack>
    </OnboardingTour>
  );
}

You may also handle the header, title, content, and footer by using the OnBoardingTour component props. In this case the OnboardingTourStep will be ignored.

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Code, Divider, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: 'Content of the Step 1',
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: 'Content of the Step 2',
      myCustomInfo: <Badge>Free trial available</Badge>,
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: 'Content of the Step 3',
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      title="The Title for all steps"
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Stack w={200} gap={32}>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Step 1
          </Button>
          <Button data-onboarding-tour-id="step-2" onClick={open}>
            Step 2
          </Button>
          <Button data-onboarding-tour-id="step-3" onClick={open}>
            Step 3
          </Button>
        </Stack>
      </Stack>
    </OnboardingTour>
  );
}

Anyway, the OnboardingTourStep are always available in the OnboardingTourController and you can use them to display any variant. Fo example,

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Code, Divider, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: 'Content of the Step 1',
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: 'Content of the Step 2',
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: 'Content of the Step 3',
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      focusRevealProps={{
        popoverProps: {
          position: 'top',
        },
      }}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      title={(tourController: OnboardingTourController) => (
        <Title c="blue" order={4}>
          {tourController.currentStep?.title as string}
        </Title>
      )}
      content={(tourController: OnboardingTourController) => (
        <Text c="lime" size="lg">
          {tourController.currentStep?.content as string}
        </Text>
      )}
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Group>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Step 1
          </Button>
          <Button data-onboarding-tour-id="step-2" onClick={open}>
            Step 2
          </Button>
          <Button data-onboarding-tour-id="step-3" onClick={open}>
            Step 3
          </Button>
        </Group>
      </Stack>
    </OnboardingTour>
  );
}

Custom entry

You may use any custom entry in the OnboardingTourStep list to customize the tour. For example, here we're going to display some extra information in the footer, by using a custom property price:

...
    {
      id: 'step-2',
      title: 'Step-2',
      description: 'Description of the Step 2',
      price: 12,
    },
...

Then we can use the footer prop to display the price:

<OnboardingTour
    ...
    footer={(onboardingTour: OnboardingTourController) => {
        if (onboardingTour.currentStep?.price) {
          return onboardingTour.currentStep?.price;
        }
        return null;
    }} />
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Code, Divider, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: 'Description of the Step 1',
      price: 12,
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: 'Description of the Step 2',
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: 'Description of the Step 3',
      price: 32,
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      footer={(onboardingTour: OnboardingTourController) => {
        if (onboardingTour.currentStep?.price) {
          return (
            <Center>
              <Text size="xs">
                Price: <Badge color="orange">${onboardingTour.currentStep?.price}</Badge>
              </Text>
            </Center>
          );
        }
        return (
          <Group grow>
            <Badge color="green">Included in all plans</Badge>
          </Group>
        );
      }}
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Stack w={200} gap={32}>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Step 1
          </Button>
          <Button data-onboarding-tour-id="step-2" onClick={open}>
            Step 2
          </Button>
          <Button data-onboarding-tour-id="step-3" onClick={open}>
            Step 3
          </Button>
        </Stack>
      </Stack>
    </OnboardingTour>
  );
}

Custom Popover Content

You may use the controller to override the default behavior of the tour or to add custom logic to the tour. For example, you could replace the default Popover content with a custom one:

import { OnboardingTour, type OnboardingTourStep } from '@gfazioli/mantine-onboarding-tour';
import { Button, Code, Divider, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

/** Your custom popover content */
import { customPopoverContent } from './customPopoverContent';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: 'Description of the Step 1',
      image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-1.png',
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: 'Description of the Step 2',
      freeTrialBadge: (
        <Center>
          <Badge color="lime">Free trial available</Badge>
        </Center>
      ),
      image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-2.png',
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: 'Description of the Step 3',
      image: 'https://raw.githubusercontent.com/mantinedev/mantine/master/.demo/images/bg-3.png',
    },
  ];  

  /** Since we are using the FocusReveal component, here we can interact and set all its props. */
  const focusRevealProps: FocusRevealProps = {
    popoverProps: {
      arrowSize: 20,
      position: 'right',
      styles: {
        dropdown: {
          padding: 0,
        },
      },
    },
  };

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      title={() => null}
      content={customPopoverContent}
      withStepper={false}
      withSkipButton={false}
      withPrevButton={false}
      withNextButton={false}
      focusRevealProps={focusRevealProps}
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Stack w={200} gap={32}>
          <Button data-onboarding-tour-id="step-1">Title</Button>
          <Button data-onboarding-tour-id="step-2">Description</Button>
          <Button data-onboarding-tour-id="step-3">Content</Button>
        </Stack>
      </Stack>
    </OnboardingTour>
  );
}

Custom Stepper

The stepper prop allows you to customize the stepper of the tour. For example, you could use it to display a progress bar or a custom list of steps. Currently, the OnboardingTour component use the Mantine Stepper component. You can use the stepperProps and stepperStepProps props to customize the stepper. Of course, you can build your own using the stepper prop.

import { FocusRevealProps } from '@gfazioli/mantine-focus-reveal';
import {
  OnboardingTour,
  OnboardingTourController,
  type OnboardingTourStep,
} from '@gfazioli/mantine-onboarding-tour';
import { Badge, Button, Center, Divider, Group, Rating, Stack } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const focusRevealProps: FocusRevealProps = {
    popoverProps: {
      position: 'top',
    },
  };

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: 'Content of the Step 1',
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: 'Content of the Step 2',
      freeTrialBadge: (
        <Center>
          <Badge color="lime">Free trial available</Badge>
        </Center>
      ),
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: 'Content of the Step 3',
    },
  ];

  const customStepper = (tourController: OnboardingTourController) => (
    <Center>
      <Rating
        count={tourController.tour.length}
        value={tourController.currentStepIndex || 0}
        onChange={(value) => tourController.setCurrentStepIndex(value - 1)}
      />
    </Center>
  );

  return (
    <OnboardingTour
      tour={onboardingSteps}
      focusRevealProps={focusRevealProps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      stepper={customStepper}
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />

        <Group>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Step 1
          </Button>
          <Button data-onboarding-tour-id="step-2" onClick={open}>
            Step 2
          </Button>
          <Button data-onboarding-tour-id="step-3" onClick={open}>
            Step 3
          </Button>
        </Group>
      </Stack>
    </OnboardingTour>
  );
}

OnboardingTour.Target

You have to use the OnboardingTour.Target when your tour component are not visible as children of the OnboardingTour component. For example, here is an example of a tour component that is not a child of the OnboardingTour component.

function AnotherComponent() {
  return (
    <Group>
       {/* ❌ This won't work */}
      <Button data-onboarding-tour-id="my-button">Click me</Button>
      <Button>Cancel</Button>
    </Group>
  );
}

...
function Demo() {

   const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'welcome',
      title: 'Welcome to the Onboarding Tour Component',
      content:
        'This is a demo of the Onboarding Tour component, which allows to create onboarding experiences for your users.',
    },
    {
      id: 'my-button',
      title: 'The New Action Button',
      content: 'This is the content for my button',
    },
  ];

  return (
    <OnboardingTour tour={onboardingSteps} started={true}>
      <div>
        <Title data-onboarding-tour-id="welcome">Welcome to the Onboarding Tour Component</Title>
        <AnotherComponent />
      </div>
    </OnboardingTour>
  )
}

The above example won't work because we can't get the data-onboarding-tour-id attribute of the AnotherComponent component. In this case you have to use the OnboardingTour.Target component to make it work. In short, instead of using the data-onboarding-tour-id attribute of the component, you have to wrap your component with OnboardingTour.Target.

function AnotherComponent() {
  return (
    <Group>
      {/* ✅ This will work */}
      <OnboardingTour.Target id="my-button">
        <Button>Click me</Button>
      </OnboardingTour.Target>
      <Button>Cancel</Button>
    </Group>
  );
}

...
function Demo() {

   const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'welcome',
      title: 'Welcome to the Onboarding Tour Component',
      content:
        'This is a demo of the Onboarding Tour component, which allows to create onboarding experiences for your users.',
    },
    {
      id: 'my-button',
      title: 'The New Action Button',
      content: 'This is the content for my button',
    },
  ];

  return (
    <OnboardingTour tour={onboardingSteps} started={true}>
      <div>
        <Title data-onboarding-tour-id="welcome">Welcome to the Onboarding Tour Component</Title>
        <AnotherComponent />
      </div>
    </OnboardingTour>
  )
}
import { OnboardingTour, type OnboardingTourStep } from '@gfazioli/mantine-onboarding-tour';
import { Badge, Button, Divider, Stack } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function AnotherComponent({ open }: { open: () => void }) {
  return (
    <>
      <OnboardingTour.Target id="step-2">
        <Button onClick={open}>Step 2</Button>
      </OnboardingTour.Target>
      <OnboardingTour.Target id="step-3">
        <Button onClick={open}>Step 3</Button>
      </OnboardingTour.Target>
    </>
  );
}

function Demo() {
  const [started, { open, close }] = useDisclosure(false);

  const onboardingSteps: OnboardingTourStep[] = [
    {
      id: 'step-1',
      title: 'Step-1',
      content: <Badge color="lime">I'm a direct child of OnboardingTour</Badge>,
    },
    {
      id: 'step-2',
      title: 'Step-2',
      content: <Badge color="yellow">I'm not a direct child of OnboardingTour</Badge>,
    },
    {
      id: 'step-3',
      title: 'Step-3',
      content: <Badge color="yellow">I'm not a direct child of OnboardingTour</Badge>,
    },
  ];

  return (
    <OnboardingTour
      tour={onboardingSteps}
      started={started}
      onOnboardingTourEnd={close}
      onOnboardingTourClose={close}
      title="The Title for all steps"
      maw={400}
    >
      <Stack justify="center" align="center">
        <Button size="md" radius={256} variant="gradient" onClick={open}>
          👉 Click here to Start the Tour 👈
        </Button>

        <Divider my={32} />
        <Stack w={200} gap={32}>
          <Button data-onboarding-tour-id="step-1" onClick={open}>
            Step 1
          </Button>
          <AnotherComponent open={open} />
        </Stack>
      </Stack>
    </OnboardingTour>
  );
}

OnboardingTour.FocusReveal

The OnboardingTour.FocusReveal component allows highlighting and making any component on your page more visible. The highlighting process can be controlled by three main properties:

  • withOverlay: displays a dark overlay across the entire page except for the highlighted component
  • withReveal: scrolls the page to make the component to be highlighted visible
  • focusEffect: applies a series of predefined effects when the component is highlighted

Naturally, we have the focused prop that controls when the component should be highlighted.

Note: In all examples, we use the onBlur event to remove focus from the component for demonstration purposes. In a real-world scenario, you might also use the focused prop to control the focus state.

A simple example of the Focus Reveal component

👉 Scroll up the page to remove the focus

John Doe

Scroll the page 👆up or 👇down to remove the focus from the card. In practice, make this component invisible so that the onBlur event will be triggered.

import { OnboardingTour, type OnboardingTourFocusRevealProps} from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Group, Stack, Text, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>A simple example of the Focus Reveal component</Title>
      <Text>👉 Scroll up the page to remove the focus</Text>

      <Center>
        <Button onClick={open}>Set the Focus to the below component</Button>
      </Center>

      <Divider my={256} />

      <Group justify="center">
        <Testimonials testimonial={0} />
        <OnboardingTour.FocusReveal focused={focused}  onBlur={close}>
          <Testimonials testimonial={1} />
        </OnboardingTour.FocusReveal>
      </Group>
    </Stack>
  );
}

Uncontrolled Mode

The OnboardingTour.FocusReveal component can also be used in an uncontrolled mode. In this mode, the component will automatically highlight the children.

Simple (uncontrolled) Example

The defaultFocused props is set to true, card below is focused by default

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Center, Code, Divider, Stack, Text, Title } from '@mantine/core';

function Demo() {

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Simple (uncontrolled) Example</Title>

      <Center>
        <Text>
          The <Code>defaultFocused</Code> props is set to <Code>true</Code>, card below is focused
          by default
        </Text>
      </Center>

      <Divider mb={600}
        label={
          <>
            <Text fz={48}>👇</Text>
            <Text>Scroll down</Text>
          </>
        }
      />

      <Center>
        <OnboardingTour.FocusReveal defaultFocused={true} withReveal={false}>
          <Testimonials testimonial={0} />
        </OnboardingTour.FocusReveal>
      </Center>

      <Divider my={200} />
    </Stack>
  );
}

OnboardingTour.FocusReveal.Group

If you want to highlight multiple components, you can use the OnboardingTour.FocusReveal.Group component. This component allows you to highlight multiple components at the same time. This also allows for controlling multiple OnboardingTour.FocusReveal components simultaneously, setting some common properties for all the components.

export interface OnboardingTourFocusRevealGroupProps {
  /** FocusReveal mode/effects when focused */
  focusedMode?: OnboardingTourFocusRevealFocusedMode;

  /** Indicator if element should be revealed. Default `false` */
  withReveal?: boolean;

  /** Will render overlay if set to `true` */
  withOverlay?: boolean;

  /** Props passed down to `Overlay` component */
  overlayProps?: OverlayProps & ElementProps<'div'>;

  /** Props passed down to the `Transition` component that used to animate the Overlay, use to configure duration and animation type, `{ duration: 150, transition: 'fade' }` by default */
  transitionProps?: TransitionOverride;

  /** Content */
  children?: React.ReactNode;
}

Note: The OnboardingTour.FocusReveal.Group component does not have the focused prop. Instead, use the focused prop in the OnboardingTour.FocusReveal component. In addition, the withReveal prop is set to false by default. The defaultFocused prop is set to true by default.

Group Example

The defaultFocused props is set to true, card below is focused by default

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Center, Code, Divider, Stack, Text, Title } from '@mantine/core';

function Demo() {
  return (
    <>
      <Stack justify="center" align="center">
        <Title order={4}>Group Example</Title>

        <Center>
          <Text>
            The <Code>defaultFocused</Code> props is set to <Code>true</Code>, card below is focused
            by default
          </Text>
        </Center>

        <Divider mb={600}
          label={
            <>
              <Text fz={48}>👇</Text>
              <Text>Scroll down</Text>
            </>
          }
        />
      </Stack>

      <OnboardingTour.FocusReveal.Group focusedMode="scale">
        <Stack>
          <Center>
            <OnboardingTour.FocusReveal>
              <Testimonials testimonial={0} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={200} />

          <Center>
            <OnboardingTour.FocusReveal>
              <Testimonials testimonial={1} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={200} />

          <Center>
            <OnboardingTour.FocusReveal>
              <Testimonials testimonial={2} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={200} />
        </Stack>
      </OnboardingTour.FocusReveal.Group>
    </>
  );
}

Below another example of using the OnboardingTour.FocusReveal.Group component.

Group Example

The defaultFocused props is set to true, card below is focused by default

import { useState } from 'react';
import { FocusRevealFocusedMode, focusRevealModes } from '@gfazioli/mantine-focus-reveal';
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Center, Code, Divider, Group, Select, Stack, Text, Title } from '@mantine/core';

function Demo() {
  const [focusedMode, setFocusedMode] = useState<string | null>('scale');

  return (
    <>
      <Stack justify="center" align="center">
        <Title order={4}>Group Example</Title>

        <Group justify="left">
          <Select
            data={[
              'border',
              'elastic',
              'glow',
              'glow-blue',
              'glow-green',
              'glow-red',
              'none',
              'pulse',
              'rotate',
              'scale',
              'shake',
              'zoom',
            ]}
            value={focusedMode}
            onChange={setFocusedMode}
            label="Focused mode"
          />
        </Group>

        <Center>
          <Text>
            The <Code>defaultFocused</Code> props is set to <Code>true</Code>, card below is focused
            by default
          </Text>
        </Center>

        <Divider
          mb={600}
          label={
            <>
              <Text fz={48}>👇</Text>
              <Text>Scroll down</Text>
            </>
          }
        />
      </Stack>

      <OnboardingTour.FocusReveal.Group focusedMode={focusedMode as FocusRevealFocusedMode}>
        <Stack>
          <Center>
            <OnboardingTour.FocusReveal>
              <Testimonials testimonial={0} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={100} />

          <Center>
            <OnboardingTour.FocusReveal defaultFocused={false}>
              <Testimonials testimonial={1} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={100} />

          <Center>
            <OnboardingTour.FocusReveal>
              <Testimonials testimonial={2} />
            </OnboardingTour.FocusReveal>
          </Center>

          <Divider my={100} />
        </Stack>
      </OnboardingTour.FocusReveal.Group>
    </>
  );
}

withReveal

The withReveal props scrolls the page to make the component to be highlighted visible. This is useful when the component is not visible on the screen. Internally, the component uses the scrollIntoView method to make the component visible, from the Mantine useScrollIntoView() hook.

Of course, you can customize the scroll behavior by using the revealProps prop. This prop accepts the same properties as the useScrollIntoView() method.

interface ScrollIntoViewParams {
  /** callback fired after scroll */
  // onScrollFinish?: () => void; // See below

  /** duration of scroll in milliseconds */
  duration?: number;

  /** axis of scroll */
  axis?: 'x' | 'y';

  /** custom mathematical easing function */
  easing?: (t: number) => number;

  /** additional distance between nearest edge and element */
  offset?: number;

  /** indicator if animation may be interrupted by user scrolling */
  cancelable?: boolean;

  /** prevents content jumping in scrolling lists with multiple targets */
  isList?: boolean;
}

Custom Reveal Props Example

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Group, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Custom Reveal Props Example</Title>

      <Center>
        <Button onClick={open}>Set the Focus to the below component</Button>
      </Center>

      <Divider my={200} />

      <Group justify="center">
        <OnboardingTour.FocusReveal
          focused={focused}
          onBlur={close}
          revealProps={{
            duration: 500,
          }}
        >
          <Testimonials testimonial={1} />
        </OnboardingTour.FocusReveal>
      </Group>

      <Divider my={200} />
    </Stack>
  );
}

Note: The onScrollFinish callback is not available in the revealsProps prop. Instead, use the onRevealFinish prop.

withOverlay

The withOverlay prop displays a dark overlay across the entire page except for the highlighted component. This is useful when you want to focus the user's attention on a specific component. The overlay is customizable by using the overlayProps prop.

interface OverlayProps {
  /** Controls overlay background-color opacity 0–1, disregarded when gradient prop is set, 0.6 by default */
  backgroundOpacity?: number;

  /** Overlay background blur, 0 by default */
  blur?: string | number;

  /** Determines whether content inside overlay should be vertically and horizontally centered, false by default */
  center?: boolean;

  /** Content inside overlay */
  children?: React.ReactNode;

  /** Overlay background-color, #000 by default */
  color?: BackgroundColor;

  /** Determines whether overlay should have fixed position instead of absolute, false by default */
  fixed?: boolean;

  /** Changes overlay to gradient. If set, color prop is ignored */
  gradient?: string;

  /** Key of theme.radius or any valid CSS value to set border-radius, 0 by default */
  radius?: MantineRadius | number;

  /** Overlay z-index, 200 by default */
  zIndex?: string | number;
}

Overlay Example

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Group, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Overlay Example</Title>

      <Center>
        <Button onClick={open}>Set the Focus to the below component</Button>
      </Center>

      <Divider my={200} />

      <Group justify="center">
        <OnboardingTour.FocusReveal
          focused={focused}
          onBlur={close}
          overlayProps={{
            color: 'rgba(255,0,0,1)',
            blur: 0,
          }}
        >
          <Testimonials testimonial={1} />
        </OnboardingTour.FocusReveal>
      </Group>

      <Divider my={200} />
    </Stack>
  );
}

scrollableRef

The scrollableRef prop allows you to specify a custom scrollable element. This is useful when the component to be highlighted is inside a scrollable container. The scrollableRef prop accepts a React.RefObject<HTMLElement>.

With simple Paper container

Paper container Example

import { useRef } from 'react';
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Paper, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);
  const scrollRef = useRef<HTMLDivElement>(null);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Paper container Example</Title>

      <Paper
        shadow="sm"
        withBorder
        radius={16}
        p={16}
        ref={scrollRef}
        h={500}
        style={{ overflow: 'auto' }}
      >
        <Center>
          <Button onClick={open}>Reveal the Bottom Card</Button>
        </Center>

        <Divider my={400} label="Divider" />

        <Center>
          <OnboardingTour.FocusReveal
            scrollableRef={scrollRef as React.RefObject<HTMLDivElement>}
            focused={focused}
            onBlur={close}
          >
            <Testimonials testimonial={0} />
          </OnboardingTour.FocusReveal>
        </Center>
      </Paper>
    </Stack>
  );
}

Below, by using the ScrollArea component, we can create a scrollable container.

ScrollArea

import { useRef } from 'react';
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, ScrollArea } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);
  const scrollRef = useRef<HTMLDivElement>(null);

  return (
    <ScrollArea viewportRef={scrollRef} h={500} style={{ position: 'relative' }}>
      <Center>
        <Button onClick={open}>Reveal the Bottom Card</Button>
      </Center>

      <Divider my={400} label="Divider" />

      <Center>
        <OnboardingTour.FocusReveal
          scrollableRef={scrollRef as React.RefObject<HTMLDivElement>}
          focused={focused}
          onBlur={close}
        >
          <Testimonials testimonial={0} />
        </OnboardingTour.FocusReveal>
      </Center>
    </ScrollArea>
  );
}

Custom Focus Mode

The OnboardingTour.FocusReveal component allows you to create a custom focus mode. This is useful when you want to create a custom focus effect.

Custom Focused Mode Example

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Group, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';
import classes from './CustomMode.module.css';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Custom Focused Mode Example</Title>

      <Center>
        <Button onClick={open}>Set the Focus to the below component</Button>
      </Center>

      <Divider my={100} label="Divider" />

      <Group justify="center">
        <OnboardingTour.FocusReveal focused={focused} onBlur={close} className={classes.custom}>
          <Testimonials testimonial={1} />
        </OnboardingTour.FocusReveal>
      </Group>
    </Stack>
  );
}

With Popover

In addition to focus and reveal, you can add a Popover display when the element gains focus by using the popoverContent prop.

Popover example

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Popover example</Title>

      <Center>
        <Button onClick={open}>Reveal the Bottom Card</Button>
      </Center>

      <Divider my={200} label="Divider" />

      <Center>
        <OnboardingTour.FocusReveal focused={focused} onBlur={close} popoverContent={<h1>Hello, World!</h1>}>
          <Testimonials testimonial={0} />
        </OnboardingTour.FocusReveal>
      </Center>
    </Stack>
  );
}

You can use the popoverProps prop (are the same as the Mantine Popover component. You can find the documentation here) to manipulate the Mantine Popover component.

Popover example

import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Center, Divider, Stack, Title } from '@mantine/core';
import { useDisclosure } from '@mantine/hooks';

function Demo() {
  const [focused, { close, open }] = useDisclosure(false);

  return (
    <Stack justify="center" align="center">
      <Title order={4}>Popover example</Title>

      <Center>
        <Button onClick={open}>Reveal the Bottom Card</Button>
      </Center>

      <Divider my={200} label="Divider" />

      <Center>
        <OnboardingTour.FocusReveal
          focused={focused}
          onBlur={close}
          popoverContent={<h1>Hello, World!</h1>}
          popoverProps={{
            position: 'top',
            withArrow: false,
            shadow: 'md',
            radius: 256,
          }}
        >
          <Testimonials testimonial={0} />
        </OnboardingTour.FocusReveal>
      </Center>
    </Stack>
  );
}

Example: Cycle

Here are some examples of how to use the OnboardingTour.FocusReveal component in different scenarios.

Cycle Example

Use the arrow keys to cycle through the testimonials

import { useState } from 'react';
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Group, Stack, Text, Title } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';

function Demo() {
  const [focusIndex, setFocusIndex] = useState(-1);
  const MAX_TESTIMONIALS = 3;

  useHotkeys([
    [
      'ArrowRight',
      () =>
        focusIndex >= 0 && setFocusIndex(focusIndex + 1 < MAX_TESTIMONIALS ? focusIndex + 1 : 0),
    ],
    [
      'ArrowLeft',
      () =>
        focusIndex >= 0 &&
        setFocusIndex(focusIndex - 1 >= 0 ? focusIndex - 1 : MAX_TESTIMONIALS - 1),
    ],
  ]);

  return (
    <Stack justify="center" align="center">
      <Title order={1}>Cycle Example</Title>
      <Text fs="italic">Use the arrow keys to cycle through the testimonials</Text>
      <Button onClick={() => setFocusIndex(0)}>Start</Button>

      <Group justify="center">
        {testimonials.map(
          (_, index) =>
            index < MAX_TESTIMONIALS && (
              <OnboardingTour.FocusReveal
                key={`focus-reveal-${index}`}
                focused={focusIndex === index}
                transitionProps={{ duration: 0, exitDuration: 0 }}
                onBlur={() => setFocusIndex(-1)}
                focusedMode="zoom"
              >
                <Testimonials key={`box-${index}`} testimonial={0}>
                  <Group justify="center">
                    <Button
                      size="xs"
                      variant="gradient"
                      onClick={() => setFocusIndex(index + 1 < MAX_TESTIMONIALS ? index + 1 : 0)}
                    >
                      Next
                    </Button>
                  </Group>
                </Testimonials>
              </OnboardingTour.FocusReveal>
            )
        )}
      </Group>
    </Stack>
  );
}

Example: Multiple Focus components

Multiple components Example

Use the arrow keys to cycle through the testimonials

import { useState } from 'react';
import { OnboardingTour } from '@gfazioli/mantine-onboarding-tour';
import { Button, Group, Paper, Stack, Text, Title } from '@mantine/core';
import { useHotkeys } from '@mantine/hooks';

function Demo() {
  const [focusIndex, setFocusIndex] = useState(-1);
  const MAX_TESTIMONIALS = 3;

  useHotkeys([
    [
      'ArrowRight',
      () =>
        focusIndex >= 0 && setFocusIndex(focusIndex + 1 < MAX_TESTIMONIALS ? focusIndex + 1 : 0),
    ],
    [
      'ArrowLeft',
      () =>
        focusIndex >= 0 &&
        setFocusIndex(focusIndex - 1 >= 0 ? focusIndex - 1 : MAX_TESTIMONIALS - 1),
    ],
  ]);

  const descriptions = [
    'This is the first description.',
    'This is the second description.',
    'This is the third description.',
    'This is the fourth description.',
  ];

  return (
    <Stack justify="center" align="center">
      <Title order={1}>Multiple components Example</Title>
      <Text fs="italic">Use the arrow keys to cycle through the testimonials</Text>
      <Button onClick={() => setFocusIndex(0)}>Start</Button>

      <Group justify="center">
        {testimonials.map(
          (_, index) =>
            index < MAX_TESTIMONIALS && (
              <OnboardingTour.FocusReveal
                key={`focus-reveal-${index}`}
                focused={focusIndex === index}
                transitionProps={{ duration: 0, exitDuration: 0 }}
                onBlur={() => setFocusIndex(-1)}
                focusedMode="zoom"
              >
                <Testimonials key={`box-${index}`} testimonial={0}>
                  <Group justify="center">
                    <Button
                      size="xs"
                      variant="gradient"
                      onClick={() => setFocusIndex(index + 1 < MAX_TESTIMONIALS ? index + 1 : 0)}
                    >
                      Next
                    </Button>
                  </Group>
                </Testimonials>
              </OnboardingTour.FocusReveal>
            )
        )}
      </Group>
      {focusIndex >= 0 && (
        <OnboardingTour.FocusReveal defaultFocused={true} withReveal={false}>
          <Paper withBorder shadow="sm" p={16} mt={32}>
            <Group>
              <Text>{descriptions[focusIndex]}</Text>
              <Button size="xs" onClick={() => setFocusIndex(-1)}>
                Stop
              </Button>
            </Group>
          </Paper>
        </OnboardingTour.FocusReveal>
      )}
    </Stack>
  );
}