Tour

Component

Interactive examples and API documentation

Basic
A simple multi-step tour.
Code
<%= render Hakumi::Space::Component.new(direction: "vertical", size: "large") do |space| %>
  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-basic-btn", type: :primary, size: :small) do %>
      Start Tour
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Space::Component.new(size: "large") do |row| %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-basic-target-1", type: :primary) do %>
          Create project
        <% end %>
      <% end %>

      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-basic-target-2") do %>
          Invite team
        <% end %>
      <% end %>

      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-basic-target-3") do %>
          Review dashboard
        <% end %>
      <% end %>
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-basic",
      open: false,
      steps: [
        { title: "Create", body: "Start by creating a new project.", target: "#tour-basic-target-1" },
        { title: "Invite", body: "Invite your collaborators.", target: "#tour-basic-target-2" },
        { title: "Review", body: "Track progress on the dashboard.", target: "#tour-basic-target-3" }
      ]
    ) %>
  <% end %>
<% end %>

<script>
  const setupTourBasic = () => {
    const tour = document.getElementById("tour-basic")
    const btn = document.getElementById("tour-basic-btn")

    if (btn && tour?.hakumiComponent?.api) {
      btn.addEventListener("click", () => {
        tour.hakumiComponent.api.open()
      })
    }
  }

  // Wait for API to be ready
  const checkBasic = setInterval(() => {
    if (document.getElementById("tour-basic")?.hakumiComponent?.api) {
      setupTourBasic()
      clearInterval(checkBasic)
    }
  }, 100)
  setTimeout(() => clearInterval(checkBasic), 5000)
</script>
Sizes
Small, default, and large tour panels.
Code
<%= render Hakumi::Space::Component.new(direction: "vertical", size: "large") do |space| %>
  <% space.with_item do %>
    <%= render Hakumi::Space::Component.new(size: "small") do |row| %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-size-btn-sm", size: :small) do %>
          Show Small
        <% end %>
      <% end %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-size-btn-md", type: :primary, size: :small) do %>
          Show Default
        <% end %>
      <% end %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-size-btn-lg", size: :small) do %>
          Show Large
        <% end %>
      <% end %>
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-size-target-sm") do %>
      Small target
    <% end %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-size-sm",
      size: "small",
      open: false,
      mask: false,
      steps: [
        { title: "Small Tour", body: "Compact sizing for tight layouts.", target: "#tour-size-target-sm" }
      ]
    ) %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-size-target-md", type: :primary) do %>
      Default target
    <% end %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-size-md",
      open: false,
      mask: false,
      steps: [
        { title: "Default Tour", body: "Balanced spacing and typography.", target: "#tour-size-target-md" }
      ]
    ) %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-size-target-lg") do %>
      Large target
    <% end %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-size-lg",
      size: "large",
      open: false,
      mask: false,
      steps: [
        { title: "Large Tour", body: "Roomy tour for more context.", target: "#tour-size-target-lg" }
      ]
    ) %>
  <% end %>
<% end %>

<script>
  const setupTourSizes = () => {
    const tours = {
      sm: document.getElementById("tour-size-sm"),
      md: document.getElementById("tour-size-md"),
      lg: document.getElementById("tour-size-lg")
    }
    const buttons = {
      sm: document.getElementById("tour-size-btn-sm"),
      md: document.getElementById("tour-size-btn-md"),
      lg: document.getElementById("tour-size-btn-lg")
    }

    Object.entries(buttons).forEach(([size, btn]) => {
      if (btn && tours[size]?.hakumiComponent?.api) {
        btn.addEventListener("click", () => {
          // Close all tours first
          Object.values(tours).forEach(tour => tour?.hakumiComponent?.api?.close())
          // Open the selected one
          tours[size].hakumiComponent.api.open()
        })
      }
    })
  }

  // Wait for API to be ready
  const checkSizes = setInterval(() => {
    if (document.getElementById("tour-size-sm")?.hakumiComponent?.api) {
      setupTourSizes()
      clearInterval(checkSizes)
    }
  }, 100)
  setTimeout(() => clearInterval(checkSizes), 5000)
</script>
Variants
Default and primary tour styles.
Code
<%= render Hakumi::Space::Component.new(direction: "vertical", size: "large") do |space| %>
  <% space.with_item do %>
    <%= render Hakumi::Space::Component.new(size: "small") do |row| %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-variant-btn-default", size: :small) do %>
          Show Default
        <% end %>
      <% end %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-variant-btn-primary", type: :primary, size: :small) do %>
          Show Primary
        <% end %>
      <% end %>
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-variant-target-default") do %>
      Default style
    <% end %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-variant-default",
      open: false,
      mask: false,
      steps: [
        { title: "Default", body: "Standard tour styling.", target: "#tour-variant-target-default" }
      ]
    ) %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-variant-target-primary", type: :primary) do %>
      Primary style
    <% end %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-variant-primary",
      type: "primary",
      open: false,
      mask: false,
      steps: [
        { title: "Primary", body: "Use for guided onboarding steps.", target: "#tour-variant-target-primary" }
      ]
    ) %>
  <% end %>
<% end %>

<script>
  const setupTourVariants = () => {
    const tours = {
      default: document.getElementById("tour-variant-default"),
      primary: document.getElementById("tour-variant-primary")
    }
    const buttons = {
      default: document.getElementById("tour-variant-btn-default"),
      primary: document.getElementById("tour-variant-btn-primary")
    }

    Object.entries(buttons).forEach(([variant, btn]) => {
      if (btn && tours[variant]?.hakumiComponent?.api) {
        btn.addEventListener("click", () => {
          // Close all tours first
          Object.values(tours).forEach(tour => tour?.hakumiComponent?.api?.close())
          // Open the selected one
          tours[variant].hakumiComponent.api.open()
        })
      }
    })
  }

  // Wait for API to be ready
  const checkVariants = setInterval(() => {
    if (document.getElementById("tour-variant-default")?.hakumiComponent?.api) {
      setupTourVariants()
      clearInterval(checkVariants)
    }
  }, 100)
  setTimeout(() => clearInterval(checkVariants), 5000)
</script>
Placement
Adjust where the tour panel appears relative to the target.
Code
<%= render Hakumi::Space::Component.new(direction: "vertical", size: "large") do |space| %>
  <% space.with_item do %>
    <%= render Hakumi::Space::Component.new(size: "small", wrap: true) do |row| %>
      <% %w[top bottom left right].each do |placement| %>
        <% row.with_item do %>
          <%= render Hakumi::Button::Component.new(
            id: "tour-placement-btn-#{placement}",
            size: :small
          ) do %>
            <%= placement.capitalize %>
          <% end %>
        <% end %>
      <% end %>
    <% end %>
  <% end %>

  <% space.with_item do %>
    <div style="display: flex; justify-content: center; align-items: center; min-height: 200px;">
      <%= render Hakumi::Button::Component.new(id: "tour-placement-target", type: :primary) do %>
        Target element
      <% end %>
    </div>
  <% end %>

  <% space.with_item do %>
    <% %w[top bottom left right].each do |placement| %>
      <%= render Hakumi::Tour::Component.new(
        id: "tour-placement-#{placement}",
        open: false,
        mask: false,
        steps: [
          {
            title: "#{placement.capitalize} placement",
            body: "Arrow points from #{placement} side.",
            target: "#tour-placement-target",
            placement: placement
          }
        ]
      ) %>
    <% end %>
  <% end %>
<% end %>

<script>
  const setupTourPlacements = () => {
    const placements = ['top', 'bottom', 'left', 'right']
    const tours = {}
    const buttons = {}

    placements.forEach(placement => {
      tours[placement] = document.getElementById(`tour-placement-${placement}`)
      buttons[placement] = document.getElementById(`tour-placement-btn-${placement}`)
    })

    placements.forEach(placement => {
      if (buttons[placement] && tours[placement]?.hakumiComponent?.api) {
        buttons[placement].addEventListener("click", () => {
          // Close all tours first
          placements.forEach(p => tours[p]?.hakumiComponent?.api?.close())
          // Open the selected one
          tours[placement].hakumiComponent.api.open()
        })
      }
    })
  }

  // Wait for API to be ready
  const checkPlacement = setInterval(() => {
    if (document.getElementById("tour-placement-top")?.hakumiComponent?.api) {
      setupTourPlacements()
      clearInterval(checkPlacement)
    }
  }, 100)
  setTimeout(() => clearInterval(checkPlacement), 5000)
</script>
Controlled
Drive the tour with the public API.
Code
<%= render Hakumi::Space::Component.new(direction: "vertical", size: "large") do |space| %>
  <% space.with_item do %>
    <%= render Hakumi::Space::Component.new(size: "small") do |row| %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-control-open", type: :primary) do %>
          Open tour
        <% end %>
      <% end %>
      <% row.with_item do %>
        <%= render Hakumi::Button::Component.new(id: "tour-control-close") do %>
          Close tour
        <% end %>
      <% end %>
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Button::Component.new(id: "tour-control-target") do %>
      Focused element
    <% end %>
  <% end %>

  <% space.with_item do %>
    <%= render Hakumi::Tour::Component.new(
      id: "tour-controlled",
      open: false,
      steps: [
        { title: "Controlled", body: "Use the API to open and close the tour.", target: "#tour-control-target" }
      ]
    ) %>
  <% end %>
<% end %>

<script>
  const tourEl = document.getElementById("tour-controlled")
  const openBtn = document.getElementById("tour-control-open")
  const closeBtn = document.getElementById("tour-control-close")
  let wired = false

  const wire = () => {
    const api = tourEl?.hakumiComponent?.api
    if (!api) return false

    openBtn?.addEventListener("click", () => api.open())
    closeBtn?.addEventListener("click", () => api.close())
    wired = true
    return true
  }

  if (!wire()) {
    const interval = setInterval(() => {
      if (wired) return
      if (wire()) clearInterval(interval)
    }, 120)

    setTimeout(() => clearInterval(interval), 5000)
  }
</script>

Tour Ruby Props

Prop Type Default Description
steps Array<Hash> [] List of steps with title, body, target, and optional placement.
open Boolean false Whether the tour is visible.
current Integer 0 Index of the current step.
size String "default" Panel size (small, default, large).
type String "default" Visual style (default, primary).
placement String "bottom" Default placement for steps.
mask Boolean true Whether to show the overlay mask.
mask_closable Boolean true Whether clicking the mask closes the tour.
closable Boolean true Whether the close button and Escape key are enabled.
show_progress Boolean true Whether to show the step counter.

JavaScript API (element.hakumiComponent.api)

Prop Type Default Description
open() Function - Open the tour.
close() Function - Close the tour.
toggle() Function - Toggle visibility.
isOpen() Function - Return whether the tour is open.
next() Function - Advance to the next step.
prev() Function - Go back to the previous step.
goTo(index) Function - Navigate to a step index.
getCurrent() Function - Return the current step index.
getState() Function - Return open, current, steps, and placement.