ngx-helper
ngx-helper
Framework-independent product tours, contextual popovers, dynamic highlights, and feature discovery for web applications.
- No framework or runtime dependencies
- TypeScript types included
- ESM and CommonJS builds
- Accessible keyboard navigation and safe text rendering
- Works with JavaScript, TypeScript, React, and other DOM-based applications
Install
npm install ngx-helper
Import the stylesheet once near your application entry point:
import { createFeatureFlow } from "ngx-helper";
import "ngx-helper/styles.css";
Quick start
Mark an element with a stable data-guide value:
<button data-guide="create-project">Create project</button>
Define and start a tour:
const featureFlow = createFeatureFlow({
tours: [
{
id: "onboarding",
showProgress: true,
steps: [
{
target: "create-project",
title: "Create your first project",
content: "Start a project from here.",
placement: "bottom",
},
],
},
],
});
featureFlow.start("onboarding");
String targets resolve to [data-guide="target-name"]. A target can also be an HTMLElement or a function returning an element.
Global configuration
const featureFlow = createFeatureFlow({
tours: [],
storage: "local", // "local", "session", a provider, or false
storageKey: "my-app:feature-flow",
defaultPlacement: "auto",
border: true,
arrow: true,
animation: true,
scrollBehavior: "smooth",
overlay: true,
missingTargetBehavior: "skip",
headless: false,
debug: false,
context: { role: "admin" },
theme: {
primary: "#6757d9",
primaryHover: "#5748c8",
background: "#ffffff",
text: "#17171c",
mutedText: "#6d6d78",
radius: 16,
shadow: "0 20px 50px rgb(0 0 0 / 0.18)",
overlayOpacity: 0.6,
tooltipWidth: 340,
},
});
Global appearance settings can be overridden on an individual tour step or highlight() call.
Update theme tokens at runtime without recreating the instance:
featureFlow.setTheme({
primary: "#0f766e",
primaryHover: "#115e59",
background: "#fffdf7",
text: "#17342f",
mutedText: "#647b76",
radius: 18,
});
Complete configuration reference
Global options
| Option | Type | Default | Purpose |
|---|---|---|---|
tours |
Tour[] |
[] |
Tours registered with the instance. |
theme |
FeatureFlowTheme |
Default theme | Global visual design tokens. |
storage |
"local" | "session" | StorageProvider | false |
"local" |
Persistence destination. Use false to disable persistence. |
storageKey |
string |
"featureflow:user-state" |
Key used by the storage provider. |
headless |
boolean |
false |
Runs state, targets, persistence, and events without rendering the default UI. |
debug |
boolean |
false |
Writes recoverable configuration errors to the console. |
defaultPlacement |
Placement |
"auto" |
Default tooltip placement. |
border |
boolean |
true |
Shows the tooltip and arrow border. |
arrow |
boolean |
true |
Shows the arrow between the tooltip and target. |
animation |
boolean |
true |
Enables tooltip and spotlight animation. |
scrollBehavior |
"smooth" | "instant" |
"smooth" |
Controls target scrolling. |
overlay |
boolean |
true |
Shows the dark page overlay around the spotlight. |
missingTargetBehavior |
"skip" | "stop" | "error" | "wait" |
"skip" |
Default behavior when a target cannot be resolved. |
context |
Record<string, unknown> |
{} |
Values used by object-based showWhen conditions. |
Theme options
const theme = {
primary: "#6757d9",
primaryHover: "#5748c8",
background: "#ffffff",
text: "#17171c",
mutedText: "#6d6d78",
radius: 16, // number in pixels, or a CSS string such as "1rem"
shadow: "0 20px 50px rgb(0 0 0 / 0.18)",
overlayOpacity: 0.6,
tooltipWidth: 340, // number in pixels, or a CSS width string
};
Every theme property is optional. Pass theme to createFeatureFlow() or pass partial updates to featureFlow.setTheme().
Tour options
| Option | Type | Default | Purpose |
|---|---|---|---|
id |
string |
Required | Unique tour identifier. |
title |
string |
— | Application-facing tour title. |
steps |
TourStep[] |
Required | Ordered tour steps. |
autoStart |
boolean |
false |
Starts the first eligible tour after stored state loads. |
remember |
boolean |
true |
Prevents completed or skipped tours from starting again. |
allowSkip |
boolean |
true |
Displays and enables the standard Skip action. |
showProgress |
boolean |
true |
Displays current of total progress. |
keyboardNavigation |
boolean |
true |
Enables Left and Right Arrow navigation. |
closeOnEscape |
boolean |
true |
Skips the tour when Escape is pressed. |
dontShowAgain |
boolean | { label?: string } |
false |
Replaces Skip with a persistent opt-out action. |
showWhen |
Condition |
— | Includes the tour only when its condition matches. |
Step options
| Option | Type | Default | Purpose |
|---|---|---|---|
id |
string |
— | Optional identifier accepted by goTo(id). |
target |
string | HTMLElement | () => HTMLElement | null |
Required | Element to highlight. String values use data-guide. |
title |
string | () => string | Promise<string> |
— | Static or runtime popover title. |
content |
string | Node | () => string | Node | Promise |
Required | Static or runtime popover content. |
placement |
Placement |
Global placement | Tooltip side and alignment. |
tone |
"default" | "error" |
"default" |
Standard or error visual treatment. |
actionLabel |
string |
"Next" / "Finish" |
Primary button text. |
primaryButtonColor |
string |
Theme primary | Primary button CSS color. |
primaryButtonHoverColor |
string |
Theme hover | Primary button hover CSS color. |
border |
boolean |
Global value | Shows or hides the tooltip and arrow border. |
arrow |
boolean |
Global value | Shows or hides the target arrow. |
animation |
boolean |
Global value | Enables or disables animation for the step. |
overlay |
boolean |
Global value | Enables or disables the dark overlay. |
spotlight |
boolean |
true |
Shows or hides the target spotlight. |
spotlightPadding |
number |
8 |
Pixels around the target spotlight. |
spotlightRadius |
number |
10 |
Spotlight corner radius in pixels. |
scrollIntoView |
boolean |
true |
Scrolls the target into view before positioning. |
waitForTarget |
boolean |
true |
Waits for a missing target to mount. |
waitForVisibility |
boolean |
false |
Waits for the target to become visible. |
timeout |
number |
5000 |
Target wait timeout in milliseconds. |
onTargetMissing |
"skip" | "stop" | "error" | "wait" |
Global value | Step-specific missing-target behavior. |
reveal |
{ trigger: FeatureFlowTarget, closeOnExit?: boolean } |
— | Opens a hidden container before resolving its target. |
completeWhen |
CompletionCondition |
— | Advances after a DOM event, application event, or custom condition. |
showWhen |
Condition |
— | Includes the step only when its condition matches. |
Highlight options
highlight() accepts the same target, content, placement, tone, button, appearance, spotlight, scrolling, waiting, missing-target, and reveal options as a tour step. It does not use id, completeWhen, or showWhen.
await featureFlow.highlight({
target: "save-button",
title: "Save your changes",
content: "Your updates are ready to save.",
placement: "top-end",
tone: "default",
actionLabel: "Understood",
primaryButtonColor: "#0f766e",
primaryButtonHoverColor: "#115e59",
border: true,
arrow: true,
animation: true,
overlay: false,
spotlight: true,
spotlightPadding: 6,
spotlightRadius: 10,
scrollIntoView: true,
waitForTarget: true,
waitForVisibility: true,
timeout: 8000,
onTargetMissing: "stop",
reveal: {
trigger: "settings-menu",
closeOnExit: true,
},
});
Target formats
// data-guide="create-project"
target: "create-project"
// Existing element
target: document.querySelector("#create-project")
// Runtime or conditionally mounted element
target: () => document.querySelector("#create-project")
Conditional tours and steps
Object conditions compare values against the global context:
const featureFlow = createFeatureFlow({
context: { role: "admin", plan: "pro" },
tours: [{
id: "admin-tour",
showWhen: { role: ["admin", "owner"], plan: "pro" },
steps: [{
target: "billing",
content: "Manage billing here.",
showWhen: { role: "admin" },
}],
}],
});
Use a synchronous or asynchronous function for application-defined logic:
showWhen: async () => currentUser.canManageBilling
Completion conditions
// Advance after a target DOM event
completeWhen: { event: "click" }
completeWhen: { event: "input" }
completeWhen: { event: "change" }
completeWhen: { event: "focus" }
// Advance after an application event
completeWhen: { appEvent: "report:ready" }
featureFlow.emit("report:ready");
// Poll a synchronous or asynchronous condition
completeWhen: {
custom: async () => Boolean(await getSavedProject()),
interval: 500,
}
Missing-target behavior
skip: continue to the next eligible step.stop: stop the current tour.error: emit anerrorevent and stop.wait: wait without a timeout until the target appears or the tour stops.
Custom storage
const storage = {
async get(key) {
return api.get(`/onboarding/${key}`);
},
async set(key, value) {
await api.put(`/onboarding/${key}`, value);
},
async remove(key) {
await api.delete(`/onboarding/${key}`);
},
};
const featureFlow = createFeatureFlow({
tours,
storage,
storageKey: "workspace:onboarding",
});
Storage errors are isolated so the host application continues working.
Headless mode and state
const featureFlow = createFeatureFlow({
tours,
headless: true,
});
const state = featureFlow.getState();
// {
// activeTour, currentStep, totalSteps, completedTours,
// target, step, active
// }
const unsubscribe = featureFlow.on("state:change", ({ state }) => {
renderCustomTourUI(state);
});
Dynamic highlights and errors
Use highlight() from form validation, API responses, click handlers, hover handlers, or any application event. The target, title, and content can be resolved at runtime:
const fieldName = "email";
const errors = { email: "Enter a valid email address" };
await featureFlow.highlight({
target: () => document.querySelector(`[name="${fieldName}"]`),
title: () => `Check ${fieldName}`,
content: () => errors[fieldName],
placement: "bottom-start",
tone: "error",
overlay: false,
actionLabel: "Fix it",
primaryButtonColor: "#e11d48",
primaryButtonHoverColor: "#be123c",
});
Call featureFlow.stop() to dismiss a highlight programmatically.
Hidden menus
Wait for an item after the user opens a menu:
{
target: "profile-settings",
content: "Manage your profile here.",
waitForVisibility: true,
}
Or let ngx-helper open the menu automatically before highlighting the item:
{
target: "profile-settings",
content: "Manage your profile here.",
waitForVisibility: true,
reveal: {
trigger: "account-menu-trigger",
closeOnExit: true,
},
}
The trigger is clicked only when the destination is absent or hidden. closeOnExit defaults to false and only closes a menu that ngx-helper opened.
Don't show me again
Enable a persistent opt-out action for a tour and optionally customize its text:
{
id: "workspace-tour",
dontShowAgain: {
label: "Don't show this tour again",
},
steps: [/* ... */],
}
Selecting the action closes the tour and prevents future starts, even when remember is false. Use featureFlow.reset("workspace-tour") to clear the preference. Passing { restart: true } to start() explicitly overrides it for that start.
Hover and inline click help
const link = document.querySelector("#workspace-roles");
link.addEventListener("mouseenter", () => {
featureFlow.highlight({
target: link,
title: "Workspace roles",
content: "Compare owners, admins, and members.",
placement: "top",
border: false,
overlay: false,
scrollIntoView: false,
actionLabel: "Got it",
});
});
link.addEventListener("mouseleave", () => featureFlow.stop());
link.addEventListener("focus", () => link.dispatchEvent(new Event("mouseenter")));
link.addEventListener("blur", () => featureFlow.stop());
For click help, call the same highlight() method from a link or text-styled button click handler. Use event.preventDefault() when help should replace navigation.
Animated and rich content
content accepts a string or DOM node. Build a node when the popover needs images, formatting, or animated text:
const content = document.createElement("div");
content.className = "welcome-content";
content.innerHTML = `
<img src="/tour-intro.png" alt="" />
<strong class="welcome-content__headline">Discover. Create. Keep moving.</strong>
`;
featureFlow.highlight({
target: "dashboard",
title: "Your command center",
content,
});
Add the animation in your application stylesheet. DOM nodes are cloned before insertion. Use trusted application-authored markup only.
React usage
ngx-helper can be used directly inside React without an additional package:
import { useEffect, useRef } from "react";
import { createFeatureFlow } from "ngx-helper";
import "ngx-helper/styles.css";
export default function App() {
const flowRef = useRef(null);
useEffect(() => {
flowRef.current = createFeatureFlow({
tours: [{
id: "dashboard",
steps: [{
target: "create-project",
title: "Create a project",
content: "Start here.",
}],
}],
});
return () => flowRef.current?.destroy();
}, []);
return (
<>
<button data-guide="create-project">Create project</button>
<button onClick={() => flowRef.current?.start("dashboard", { restart: true })}>
Show tour
</button>
</>
);
}
Tour and step controls
const tour = {
id: "workspace-tour",
title: "Workspace tour",
autoStart: false,
remember: true,
allowSkip: true,
showProgress: true,
keyboardNavigation: true,
closeOnEscape: true,
dontShowAgain: { label: "Don't show this tour again" },
showWhen: { role: "admin" },
steps: [
{
id: "create-step",
target: "create-project",
title: "Create a project",
content: "Click the button to continue.",
placement: "bottom-end",
tone: "default",
actionLabel: "Next",
primaryButtonColor: "#6757d9",
primaryButtonHoverColor: "#5748c8",
border: true,
arrow: true,
animation: true,
overlay: true,
spotlight: true,
spotlightPadding: 8,
spotlightRadius: 12,
scrollIntoView: true,
waitForTarget: true,
waitForVisibility: false,
timeout: 5000,
onTargetMissing: "skip",
reveal: {
trigger: "project-menu-trigger",
closeOnExit: true,
},
completeWhen: { event: "click" },
showWhen: { role: ["admin", "owner"] },
},
],
};
Supported placements are auto, top, bottom, left, and right, including -start and -end variants.
Programmatic API
await featureFlow.start("onboarding", { restart: true });
await featureFlow.next();
await featureFlow.previous();
await featureFlow.goTo(2);
await featureFlow.goTo("create-step");
await featureFlow.skip();
await featureFlow.complete();
featureFlow.stop();
featureFlow.getState();
featureFlow.setTheme({ primary: "#0f766e" });
featureFlow.emit("report:ready");
await featureFlow.reset("onboarding");
await featureFlow.resetAll();
featureFlow.destroy();
Events
const unsubscribe = featureFlow.on("step:view", ({ tourId, stepIndex, target }) => {
analytics.track("Tour step viewed", { tourId, stepIndex, target });
});
unsubscribe();
Use featureFlow.off(eventName, callback) when you retain the original callback instead of the returned unsubscribe function.
Events include tour:start, tour:stop, tour:skip, tour:complete, step:view, step:complete, target:missing, state:change, and error.
Accessibility and security
The default renderer provides keyboard navigation, Escape handling, labelled dialogs, live updates, visible focus styles, reduced-motion support, and forced-color support. String content is assigned with textContent instead of injected as HTML.
The package does not access the DOM or browser storage during module evaluation, so it can be imported in SSR applications. DOM work starts only when a tour or highlight is shown in a browser.
License
MIT