# ngx-helper

> Framework-independent contextual onboarding, product tours, and highlights

Latest version **2.0.0** (published 2026-09-11) · MIT license · 0 weekly downloads

## Install

```sh
npm install ngx-helper
pnpm add ngx-helper
yarn add ngx-helper
bun add ngx-helper
```

## Health

**Score 65/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score.

Warnings: low downloads.

## Facts

| | |
|---|---|
| Version | 2.0.0 |
| Published | 2026-09-11 |
| First published | 2019-07-14 |
| Weekly downloads | 0 |
| License | MIT |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Dependencies | 0 |
| Unpacked size | 110.1 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| Maintainers | muhs!n |
| Keywords | feature-discovery, onboarding, product-tour, tooltip, typescript |

## Links

- npm: https://www.npmjs.com/package/ngx-helper
- Homepage: https://ngx-helper-demo.muhsin642629.chatgpt.site
- npm.io page: https://npm.io/package/ngx-helper

## Alternatives

- [@openai/codex-sdk](https://npm.io/package/@openai/codex-sdk.md) — 731.4K weekly downloads
- [babel-plugin-transform-react-jsx](https://npm.io/package/babel-plugin-transform-react-jsx.md) — 565.0K weekly downloads
- [babel-helper-remove-or-void](https://npm.io/package/babel-helper-remove-or-void.md) — 508.5K weekly downloads
- [@pnpm/store-controller-types](https://npm.io/package/@pnpm/store-controller-types.md) — 186.9K weekly downloads
- [react-native-signature-canvas](https://npm.io/package/react-native-signature-canvas.md) — 155.6K weekly downloads

## Recent versions

- 2.0.0 (latest) — 2026-09-11
- 1.1.1 — 2026-08-24
- 1.1.0 — 2026-08-24
- 1.0.0 — 2026-08-24
- 0.0.9 — 2020-04-08
- 0.0.8 — 2019-12-06
- 0.0.7 — 2019-12-05
- 0.0.6 — 2019-12-05
- 0.0.5 — 2019-07-20
- 0.0.4 — 2019-07-19
- 0.0.3 — 2019-07-16
- 0.0.2 — 2019-07-15
- 0.0.1 — 2019-07-14

## README

# 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

```bash
npm install ngx-helper
```

Import the stylesheet once near your application entry point:

```js
import { helper } from "ngx-helper";
import "ngx-helper/styles.css";
```

## Quick start

Mark an element with a stable `data-guide` value:

```html
<button data-guide="create-project">Create project</button>
```

Define and start a tour:

```js
const featureFlow = helper({
  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

```js
const featureFlow = helper({
  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:

```js
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. |
| `loader` | `LoaderOptions` | Disabled | Default synchronous loader for async title/content: `true`, text, DOM node, template, or factory. |
| `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 \| "auto"` | `true` | Points the arrow toward the target automatically; `false` hides it. |
| `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

```js
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 `helper()` 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` | `DynamicValue<FeatureFlowContent>` | Required | Text, DOM node, interactive template, or a function returning one (optionally asynchronously). |
| `loader` | `LoaderOptions` | Global value | Content shown while resolving title/content. `false` disables an inherited loader. |
| `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 \| "auto"` | Global value | Automatic target arrow; `false` hides it. |
| `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, loader, placement, tone, button, appearance, spotlight, scrolling, waiting, missing-target, and reveal options as a tour step. It does not use `id`, `completeWhen`, or `showWhen`.

```js
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

```js
// 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`:

```js
const featureFlow = helper({
  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:

```js
showWhen: async () => currentUser.canManageBilling
```

### Completion conditions

```js
// 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 an `error` event and stop.
- `wait`: wait without a timeout until the target appears or the tour stops.

### Custom storage

```js
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 = helper({
  tours,
  storage,
  storageKey: "workspace:onboarding",
});
```

Storage errors are isolated so the host application continues working.

### Headless mode and state

```js
const featureFlow = helper({
  tours,
  headless: true,
});

const state = featureFlow.getState();
// {
//   activeTour, currentStep, totalSteps, completedTours,
//   target, step, active, loading
// }

const unsubscribe = featureFlow.on("state:change", ({ state }) => {
  renderCustomTourUI(state);
});
```

## Dynamic highlights and errors

### Automatic placement and arrow

Use `placement: "auto"` (the default) and `arrow: true` or `arrow: "auto"`. The helper chooses an available side of the target, and the arrow points back at that target. For click/hover help, pass the clicked or hovered element as `target`. A `reveal.trigger` only opens a menu; the arrow still points at the destination `target`.

```js
button.addEventListener("click", (event) => {
  const target = event.currentTarget;
  void featureFlow.highlight({
    target,
    placement: "auto",
    arrow: "auto",
    tone: "default",
    content: "Help for this button.",
  });
});
```

Explicit placements such as `"right-start"` are preferred sides: if there is not enough room, the helper flips or chooses another side while preserving alignment. It recalculates on scrolling, window resizing, target/content resizing, and image loading. `arrow: false` still hides the arrow.

### Custom loader and async content

```js
const featureFlow = helper({
  loader: true, // built-in "Loading…" text; disabled by default
});

await featureFlow.highlight({
  target: "profile",
  placement: "auto",
  tone: "default",
  loader: () => {
    const loader = document.createElement("div");
    loader.className = "my-loader";
    loader.textContent = "Fetching your profile…";
    return loader;
  },
  content: async ({ target, signal }) => {
    const response = await fetch("/api/profile", { signal });
    if (!response.ok) throw new Error("Profile could not be loaded");
    const profile = await response.json();
    return `Welcome, ${profile.name}!`;
  },
});
```

`loader` accepts `true`, `false`, a string, a DOM node (including an HTML template), a `{ render(container) }` template, or a synchronous function returning any of those content forms. Use CSS on your custom loader for a spinner or skeleton. It can be configured globally, per tour step, or per highlight.

The loader appears once the target has been found, while dynamic title/content resolves. Title and content resolve concurrently; the final content appears when both are ready. Loading disables Next/Finish and sets `aria-busy` and `getState().loading`. A loading highlight has a Cancel button. `loader: false` disables the loading UI but still waits for content.

Resolvers receive `{ target, signal }`. Existing zero-argument functions continue to work. Stop, destroy, or navigation aborts the signal, removes the loader, and ignores stale results or errors. Pass `signal` to `fetch` to cancel the network request too. A current content/template error emits `error` and closes the helper. Resolve or reject your content promise to finish loading; the target `timeout` does not limit content requests.

### Custom templates, including React

Yes: render a template inside the helper's content area. Title, progress, arrow, and navigation remain supplied by the helper.

For static HTML, pass an HTML template element or its cloned fragment:

```html
<template id="helper-card">
  <article>
    <h3>Workspace settings</h3>
    <p>Manage your team and preferences here.</p>
  </article>
</template>
```

```js
await featureFlow.highlight({
  target: "settings",
  tone: "default",
  content: document.querySelector("#helper-card"),
});
```

For interactive content, use a render callback. Return cleanup to release listeners, subscriptions, or framework roots when content is replaced or the helper closes:

```js
await featureFlow.highlight({
  target: "settings",
  tone: "default",
  content: {
    render(container) {
      const button = document.createElement("button");
      button.textContent = "Open preferences";
      const onClick = () => window.location.assign("/preferences");
      button.addEventListener("click", onClick);
      container.append(button);
      return () => button.removeEventListener("click", onClick);
    },
  },
});
```

React JavaScript example (inside your component/event handler, where `flow` is the helper instance):

```jsx
import { createRoot } from "react-dom/client";

function ProfileCard({ profile }) {
  return <article>
    <h3>{profile.name}</h3>
    <button onClick={() => window.location.assign("/profile")}>View profile</button>
  </article>;
}

async function showProfile(event) {
  const target = event.currentTarget;
  await flow.highlight({
    target,
    placement: "auto",
    tone: "default",
    loader: {
      render(container) {
        const root = createRoot(container);
        root.render(<p role="status">Loading profile…</p>);
        return () => root.unmount();
      },
    },
    content: async ({ signal }) => {
      const response = await fetch("/api/profile", { signal });
      if (!response.ok) throw new Error("Profile request failed");
      const profile = await response.json();
      return {
        render(container) {
          const root = createRoot(container);
          root.render(<ProfileCard profile={profile} />);
          return () => root.unmount();
        },
      };
    },
  });
}
// <button onClick={showProfile}>Profile help</button>
```

JSX cannot be passed directly as `content`. A separately mounted React root does not inherit your application's context providers; wrap the template in the providers it needs or pass data via props. The `render` callback must mount synchronously and return cleanup synchronously. Fetch data in the async `content` resolver before returning the template. Cleanup runs while its container is still attached. In headless mode templates/loaders are not mounted.

Plain DOM nodes are cloned, so their `addEventListener` handlers and framework state are not copied. Use `render` for interactive content. Strings are always safe text, not HTML.

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:

```js
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:

```js
{
  target: "profile-settings",
  content: "Manage your profile here.",
  waitForVisibility: true,
}
```

Or let ngx-helper open the menu automatically before highlighting the item:

```js
{
  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:

```js
{
  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

```js
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:

```js
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.

## Upgrading to 2.0

The factory is now named `helper`. Replace imports and calls to
`createFeatureFlow` with `helper`; the old factory name is no longer exported.
Version 2.0 also adds automatic arrow placement, asynchronous content loaders,
interactive templates, and the `showCloseButton` / `hideActionButton` flags.

## React usage

### Close icon, hidden action button, and interactive parent content

Set `showCloseButton: true` to add an accessible close icon and
`hideActionButton: true` to remove the primary Got it/Dismiss/Next/Finish button.
Both default to `false` and work on `highlight()` options and individual tour steps.
The close icon stops the helper without marking the tour completed or skipped.
Back, Skip, and Don't show me again controls are unaffected.

Use `content: { render(container) { ... } }` for interactive application content.
Plain DOM nodes are cloned, so their event listeners are not retained. A render
template mounts live content and returns cleanup that runs when the popup closes.

```jsx
import { useEffect, useRef, useState } from "react";
import { createRoot } from "react-dom/client";
import { helper } from "ngx-helper";
import "ngx-helper/styles.css";

function CopyContent({ text }) {
  const [status, setStatus] = useState("");
  async function copy() {
    try {
      await navigator.clipboard.writeText(text);
      setStatus("Copied!");
    } catch {
      setStatus("Could not copy. Please select and copy the text manually.");
    }
  }
  return (
    <div>
      <span>{text}</span>{" "}
      <button type="button" onClick={copy} aria-label="Copy text" title="Copy text">
        <svg width="16" height="16" viewBox="0 0 24 24" fill="none"
          stroke="currentColor" strokeWidth="2" aria-hidden="true">
          <rect x="9" y="9" width="12" height="12" rx="2" />
          <path d="M5 15H4a1 1 0 0 1-1-1V4a1 1 0 0 1 1-1h10a1 1 0 0 1 1 1v1" />
        </svg>
      </button>
      <p role="status">{status}</p>
    </div>
  );
}

export default function CopyHelp({ text }) {
  const flowRef = useRef(null);
  useEffect(() => {
    const flow = helper({ storage: false });
    flowRef.current = flow;
    return () => { flow.destroy(); flowRef.current = null; };
  }, []);

  function open(event) {
    void flowRef.current?.highlight({
      target: event.currentTarget,
      tone: "default",
      showCloseButton: true,
      hideActionButton: true,
      overlay: false,
      spotlight: false,
      content: {
        render(container) {
          const root = createRoot(container);
          root.render(<CopyContent text={text} />);
          return () => root.unmount();
        },
      },
    });
  }

  return <button onClick={open}>Show copy helper</button>;
}
```

Clipboard access requires HTTPS or localhost and may be denied by the browser.
This independent React root does not inherit parent context: pass props or wrap
it in the required providers. The text is captured when the popup opens; reopen
the helper to use a changed prop. For clickable popup content, do not close it
immediately on the trigger's mouseleave or blur.

ngx-helper can be used directly inside React without an additional package:

```jsx
import { useEffect, useRef } from "react";
import { helper } from "ngx-helper";
import "ngx-helper/styles.css";

export default function App() {
  const flowRef = useRef(null);

  useEffect(() => {
    flowRef.current = helper({
      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

```js
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

```js
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

```js
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

---
_Source: https://npm.io/package/ngx-helper · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
