VayuUI

Tour

A step-by-step guided tour component that highlights elements with a spotlight overlay and positions a popover with navigation controls.

Installation

npx vayu-ui-cli@latest add tour

Usage

Tour Demo

Feature A

Feature B

import { Tour, type TourStep, Typography, Button, Divider } from 'vayu-ui';

const steps: TourStep[] = [
  {
    target: '#tour-title',
    title: 'Welcome!',
    content: 'This is the Tour component. It highlights elements on the page and guides the user step-by-step.',
    placement: 'bottom',
  },
  {
    target: '#tour-card-1',
    title: 'Feature Cards',
    content: 'Each card represents a feature. The spotlight draws attention to the relevant area.',
    placement: 'right',
  },
  {
    target: '#tour-card-2',
    title: 'Second Card',
    content: 'Navigate with arrow keys, or use the Previous / Next buttons.',
    placement: 'left',
  },
  {
    target: '#tour-cta',
    title: 'Call to Action',
    content: 'Click Finish to complete the tour. You can also press Escape or click Skip at any time.',
    placement: 'top',
  },
];

export function Example() {
  const [open, setOpen] = useState(false);

  return (
    <Tour
      steps={steps}
      isOpen={open}
      onClose={() => setOpen(false)}
      onComplete={() => setOpen(false)}
      onSkip={() => setOpen(false)}
    >
      <div className="flex flex-col gap-4">
        <Typography.H3 id="tour-title">Tour Demo</Typography.H3>

        <div className="grid grid-cols-2 gap-3">
          <div id="tour-card-1" className="p-4 rounded-surface border border-border bg-surface">
            <Typography.P>Feature A</Typography.P>
          </div>
          <div id="tour-card-2" className="p-4 rounded-surface border border-border bg-surface">
            <Typography.P>Feature B</Typography.P>
          </div>
        </div>

        <Divider spacing="sm" decorative />

        <Button
          id="tour-cta"
          onClick={() => setOpen(true)}
          variant="primary"
          size="medium"
        >
          <Button.Text>Start Tour</Button.Text>
        </Button>
      </div>
    </Tour>
  );
}

Anatomy

<Tour
  steps={steps}
  isOpen={open}
  onClose={() => setOpen(false)}
  onComplete={() => setOpen(false)}
  onSkip={() => setOpen(false)}
  showProgress={true}
  showStepNumbers={true}
  maskClickable={false}
  closeOnEscape={true}
  closeOnMaskClick={false}
  scrollBehavior="smooth"
  highlightedAreaClassName="custom-spotlight"
  maskClassName="custom-mask"
>
  {/* Your application content — target elements identified by CSS selectors */}
</Tour>

Accessibility

  • Keyboard navigation: ArrowRight advances to the next step, ArrowLeft goes to the previous step, Escape closes the tour (when closeOnEscape is true)
  • ARIA attributes: role="dialog" with aria-modal="true" on the popover, aria-labelledby referencing the dialog title, role="progressbar" with aria-valuenow, aria-valuemin, aria-valuemax, and aria-label on the progress bar
  • Focus management: Body scroll is locked during the tour; the target element is scrolled into view with block: 'center' and inline: 'center'
  • Screen reader live region: A visually hidden div with aria-live="polite" and aria-atomic="true" announces step changes

Screen reader behavior

When a tour step changes, the live region announces: "Step N of M: [step title]". The popover is identified as a dialog via role="dialog" and aria-modal="true", informing screen readers that content outside the tour is temporarily inert. The progress bar conveys completion percentage through role="progressbar" with current, minimum, and maximum values. All navigation buttons include descriptive aria-label attributes for context.

Component Folder Structure

Tour/
├── Tour.tsx              # Root component with TourContext, state management, keyboard handling
├── TourOverlay.tsx       # SVG mask overlay with spotlight cutout
├── TourPopover.tsx       # Dialog popover with arrow, header, body, progress, footer
├── use-position.ts       # Popover position calculation with viewport clamping
├── use-target.ts         # Target element finding, MutationObserver, scroll/resize handling
├── hooks.ts              # useTour context consumer hook
├── types.ts              # TypeScript interfaces (TourStep, TourContextValue, TourProps)
├── index.ts              # Public exports
└── README.md             # Internal documentation

Props

Tour

PropTypeDefaultDescription
stepsTourStep[]Array of tour step definitions
isOpenbooleanfalseControlled open state
onClose() => voidCallback fired when the tour closes
onComplete() => voidCallback fired when the last step completes
onSkip() => voidCallback fired when the user skips
showProgressbooleantrueShow progress bar
showStepNumbersbooleantrueShow step counter badge
maskClickablebooleanfalseAllow pointer events through overlay
closeOnEscapebooleantrueClose on Escape key
closeOnMaskClickbooleanfalseClose when clicking the overlay
scrollBehaviorScrollBehavior"smooth"Scroll-into-view behavior
highlightedAreaClassNamestringClass name for the spotlight border
maskClassNamestringClass name for the overlay mask

TourStep

PropTypeDefaultDescription
targetstringCSS selector for the target element
titlestringStep heading
contentstring | ReactNodeStep body
placement'top' | 'bottom' | 'left' | 'right' | 'center'"bottom"Popover placement
spotlightPaddingnumber8Spotlight cutout padding
disableInteractionbooleanfalseBlock pointer events on target
showSkipbooleantrueShow skip button for this step
nextButtonTextstring"Next"Override next button label
prevButtonTextstring"Previous"Override previous button label
onNext() => void | Promise<void>Async hook before advancing
onPrev() => void | Promise<void>Async hook before going back
customButtonsReactNodeReplace the default button row

useTour Hook

const { isOpen, currentStep, steps, goToStep, nextStep, prevStep, close, skip } = useTour();

Access tour state and controls from any component rendered inside <Tour>. Throws an error if used outside the Tour provider.

On this page