Marble UI
← Components

Tour / Coachmarks

Preview: Default

Guided product tour: a popover anchored to each step's target element, with a highlight ring on the target.

Import

import { Tour } from '@marbleui/ui'
import { useTourRefs } from '@marbleui/ui'

Props

<Tour>

PropTypeDefaultDescription
isOpen *requiredboolean—
onOpenChange *required(open: boolean) => void—
steps *requiredTourStep[]—
onFinish() => void—

Hooks

useTourRefs<K extends string>(...keys: K[]): Record<K, RefObject<HTMLElement | null>>

Accessibility

Keyboard interaction, focus management and ARIA semantics come from the underlying primitives: React Aria Dialog · React Aria Heading · React Aria Popover. Follow the primitive's docs for the full keyboard map.

Usage notes

Guided product tour: a popover anchored to each step’s target element with a highlight ring. Use for first-run onboarding; for a one-off explanation use Popover or ToggleTip.

const r = useTourRefs('search', 'new')

<Button ref={r.new} className={tourTarget}>New</Button>
<Tour
  isOpen={open}
  onOpenChange={setOpen}
  onFinish={markTourSeen}
  steps={[{ target: r.new, title: 'Create', body: 'Start a new issue.' }]}
/>
  • Controlled only: you own isOpen and onOpenChange. The tour restarts at step 1 whenever it opens.
  • Each step needs a target ref; add tourTarget to the target’s className to show the ring. Targets must accept a ref, so wrap components that don’t in a span.
  • placement per step defaults to bottom. onFinish fires only when Done is pressed on the last step, not when closed early.
  • The popover is non-modal and has a close button, Back and Next.
Example source · show
import { useState } from 'react'
import { Button } from '../button'
import { SearchField } from '../search-field'
import { Tour, tourTarget, useTourRefs } from './tour'

function Demo() {
  const [open, setOpen] = useState(false)
  const r = useTourRefs('search', 'new', 'help')
  return (
    <div className="flex flex-col items-center gap-6">
      <div className="flex w-[30rem] items-center gap-2 rounded-xl border border-border bg-surface p-2">
        <div ref={r.search as React.RefObject<HTMLDivElement>} className={`flex-1 rounded-md ${tourTarget}`}>
          <SearchField aria-label="Search" variant="filled" />
        </div>
        <span ref={r.new} className={`rounded-md ${tourTarget}`}><Button>New</Button></span>
        <span ref={r.help} className={`rounded-md ${tourTarget}`}><Button variant="ghost">Help</Button></span>
      </div>
      <Button variant="soft" onPress={() => setOpen(true)}>Start tour</Button>
      <Tour
        isOpen={open}
        onOpenChange={setOpen}
        steps={[
          { target: r.search, title: 'Search everything', body: 'Find issues, docs and people. Press ⌘K from anywhere.' },
          { target: r.new, title: 'Create', body: 'Start a new issue, doc or project.' },
          { target: r.help, title: 'Need a hand?', body: 'Guides, shortcuts and support live here.', placement: 'top' },
        ]}
      />
    </div>
  )
}

export const styles = { Default: Demo }

Source: packages/ui/src/tour/tour.tsx

Catalog

Category
overlays
Priority
Core
Primitive
composed
React Aria
Popover
Base UI
Popover