Skip to content

@vibe-labs/design-vue-hotspots

A registration and discovery system for interactive hotspots. Provides declarative markup for hotspot regions and an imperative control plane for activating, deactivating, and querying them. No built-in visuals — consumers decide what "active" looks like.

Installation

ts
import { VibeHotspot, useHotspots, useHotspotsContext } from "@vibe-labs/design-vue-hotspots";

No CSS package required — this is purely behavioural.

Components

<VibeHotspot>

Wrapper component that registers a hotspot region. Registers on mount, unregisters on unmount.

Usage

vue
<!-- 1. Establish a registry at the page/layout level -->
<script setup lang="ts">
import { useHotspots, VibeHotspot } from "@vibe-labs/design-vue-hotspots";

const hotspots = useHotspots();
</script>

<!-- 2. Mark hotspot regions -->
<VibeHotspot id="revenue-chart" :meta="{ group: 'tour', step: 1 }">
  <template #default="{ active, data }">
    <RevenueChart :class="{ 'pulse': active }" />
    <HelpPanel v-if="active" :text="data?.helpText" />
  </template>
</VibeHotspot>

Props

PropTypeDefaultDescription
idstringRequired. Unique hotspot identifier.
metaRecord<string, unknown>{}Static metadata (e.g. group, step, label).
tagstring"div"HTML tag for the wrapper element.

Scoped Slot Props

PropTypeDescription
activebooleanWhether this hotspot is currently active.
dataRecord<string, unknown> | nullData passed by the consumer on activation.
entryHotspotEntry | undefinedFull registry entry (el, rect, meta, active, data).

Events

EventPayloadDescription
activatedRecord<string, unknown> | nullFired when this hotspot is activated.
deactivatedFired when this hotspot is deactivated.

Data Attributes

AttributeValueDescription
data-hotspot{id}Always present. The hotspot identifier.
data-hotspot-active(presence)Present only when active. Target with CSS.

Composables

useHotspots()

Creates a hotspot registry, provides it to the subtree via provide(), and returns the HotspotRegistry control plane. Call once per scope.

ts
import { useHotspots } from "@vibe-labs/design-vue-hotspots";

const hotspots = useHotspots();

// Activate with optional data
hotspots.activate("revenue-chart", { helpText: "This shows Q3 projections" });

// Deactivate
hotspots.deactivate("revenue-chart");
hotspots.deactivateAll();

// Query
hotspots.isActive("revenue-chart");   // boolean
hotspots.get("revenue-chart");        // HotspotEntry | undefined
hotspots.activeIds.value;             // string[]
hotspots.activeEntries.value;         // HotspotEntry[]
hotspots.entries.value;               // HotspotEntry[] (all)

// Refresh bounding rects (e.g. after scroll or layout shift)
hotspots.refresh();                   // all
hotspots.refresh("revenue-chart");    // single

useHotspotsContext()

Injects an existing registry from an ancestor. Throws if none found. Use this in child components that need access to the control plane without creating a new scope.

ts
import { useHotspotsContext } from "@vibe-labs/design-vue-hotspots";

const hotspots = useHotspotsContext(); // throws if no ancestor called useHotspots()

Types

ts
interface HotspotEntry {
  readonly id: string;
  readonly el: HTMLElement;
  rect: DOMRect;
  readonly meta: HotspotMeta;
  active: boolean;
  data: HotspotActivationData | null;
}

interface HotspotRegistry {
  register(id: string, el: HTMLElement, meta?: HotspotMeta): void;
  unregister(id: string): void;
  activate(id: string, data?: HotspotActivationData): void;
  deactivate(id: string): void;
  deactivateAll(): void;
  isActive(id: string): boolean;
  get(id: string): HotspotEntry | undefined;
  refresh(id?: string): void;
  readonly entries: ComputedRef<HotspotEntry[]>;
  readonly activeIds: ComputedRef<string[]>;
  readonly activeEntries: ComputedRef<HotspotEntry[]>;
}

Example: Guided Tour

vue
<script setup lang="ts">
import { useHotspots, VibeHotspot } from "@vibe-labs/design-vue-hotspots";

const hotspots = useHotspots();

const tourSteps = [
  { id: "nav-bar", helpText: "Use the nav bar to switch sections." },
  { id: "revenue-chart", helpText: "This shows your revenue trends." },
  { id: "settings-btn", helpText: "Configure your dashboard here." },
];

let currentStep = 0;

function startTour() {
  const step = tourSteps[currentStep];
  hotspots.activate(step.id, { helpText: step.helpText });
}

function nextStep() {
  hotspots.deactivateAll();
  currentStep++;
  if (currentStep < tourSteps.length) {
    const step = tourSteps[currentStep];
    hotspots.activate(step.id, { helpText: step.helpText });
  }
}
</script>

Dependencies

  • vue (peer)
  • @vibe-labs/core (runtime)

No CSS dependencies — this package is purely behavioural.


Usage Guide

Setup

ts
import { VibeHotspot, useHotspots, useHotspotsContext } from "@vibe-labs/design-vue-hotspots";
// No CSS import needed — purely behavioural

VibeHotspot + useHotspots

Guided tour

vue
<script setup lang="ts">
import { VibeHotspot, useHotspots } from "@vibe-labs/design-vue-hotspots";
import { ref } from "vue";

// Call useHotspots once at the page/layout level to create the registry
const hotspots = useHotspots();

const tourSteps = [
  { id: "search-bar", helpText: "Use the search bar to find tracks by BPM, key, or genre." },
  { id: "filter-panel", helpText: "Narrow results with the filters on the left." },
  { id: "results-grid", helpText: "Click any track to preview it." },
];

const currentStep = ref(-1);
const isTourActive = computed(() => currentStep.value >= 0);

function startTour() {
  currentStep.value = 0;
  activateStep();
}

function activateStep() {
  hotspots.deactivateAll();
  const step = tourSteps[currentStep.value];
  if (step) hotspots.activate(step.id, { helpText: step.helpText });
}

function nextStep() {
  currentStep.value++;
  if (currentStep.value < tourSteps.length) {
    activateStep();
  } else {
    hotspots.deactivateAll();
    currentStep.value = -1;
  }
}
</script>

<template>
  <div>
    <VibeButton @click="startTour">Start Tour</VibeButton>

    <VibeHotspot id="search-bar">
      <template #default="{ active, data }">
        <SearchBar :class="{ highlighted: active }" />
        <TourTooltip v-if="active" :text="data?.helpText" @next="nextStep" />
      </template>
    </VibeHotspot>

    <VibeHotspot id="filter-panel">
      <template #default="{ active, data }">
        <FilterPanel :class="{ highlighted: active }" />
        <TourTooltip v-if="active" :text="data?.helpText" @next="nextStep" />
      </template>
    </VibeHotspot>

    <VibeHotspot id="results-grid">
      <template #default="{ active, data }">
        <ResultsGrid :class="{ highlighted: active }" />
        <TourTooltip v-if="active" :text="data?.helpText" @next="nextStep" />
      </template>
    </VibeHotspot>
  </div>
</template>

CSS-driven highlight via data attribute

vue
<style>
/* Style active hotspots with the data attribute — no JS class needed */
[data-hotspot-active] {
  outline: 2px solid var(--color-accent);
  outline-offset: 4px;
  border-radius: var(--radius-md);
}
</style>

<template>
  <VibeHotspot id="my-feature">
    <template #default>
      <MyFeatureComponent />
    </template>
  </VibeHotspot>
</template>

useHotspotsContext

Accessing the registry from a child component

vue
<script setup lang="ts">
import { useHotspotsContext } from "@vibe-labs/design-vue-hotspots";

// Injects the registry established by an ancestor's useHotspots() call
const hotspots = useHotspotsContext();

function highlightRevenue() {
  hotspots.activate("revenue-chart", { label: "Key metric" });
}
</script>

Common Patterns

React to activation events

vue
<template>
  <VibeHotspot
    id="revenue-chart"
    :meta="{ category: 'finance' }"
    @activated="onActivated"
    @deactivated="onDeactivated"
  >
    <template #default="{ active }">
      <RevenueChart :pulse="active" />
    </template>
  </VibeHotspot>
</template>

Query all active hotspots

vue
<script setup lang="ts">
import { useHotspots } from "@vibe-labs/design-vue-hotspots";

const hotspots = useHotspots();

// Reactive list of all currently active IDs
const activeIds = hotspots.activeIds;
// e.g. activeIds.value → ["revenue-chart", "nav-bar"]
</script>

Vibe