Appearance
@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
| Prop | Type | Default | Description |
|---|---|---|---|
id | string | — | Required. Unique hotspot identifier. |
meta | Record<string, unknown> | {} | Static metadata (e.g. group, step, label). |
tag | string | "div" | HTML tag for the wrapper element. |
Scoped Slot Props
| Prop | Type | Description |
|---|---|---|
active | boolean | Whether this hotspot is currently active. |
data | Record<string, unknown> | null | Data passed by the consumer on activation. |
entry | HotspotEntry | undefined | Full registry entry (el, rect, meta, active, data). |
Events
| Event | Payload | Description |
|---|---|---|
activated | Record<string, unknown> | null | Fired when this hotspot is activated. |
deactivated | — | Fired when this hotspot is deactivated. |
Data Attributes
| Attribute | Value | Description |
|---|---|---|
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"); // singleuseHotspotsContext()
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 behaviouralVibeHotspot + 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>