npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

ngx-tour-guide

v0.2.0

Published

Angular 12+ bindings for tour-guide-core: step-by-step product tour / onboarding guide with spotlight, popover and keyboard navigation.

Readme

ngx-tour-guide

Angular bindings for tour-guide-core: step-by-step product tours / onboarding with a spotlight and a popover.

  • Angular 12 → 22 with a single package (built with Angular 12 tooling, Ivy partial compilation). RxJS 6 or 7. No CDK.
  • Declarative (tourStep directive) or programmatic (TourService.start(steps))
  • Custom step content via ng-template; string content is sanitized with DomSanitizer
  • Onboarding built in: once runs a tour one time per person (pluggable store, versioned keys), startWhen holds it until the screen is ready — a getter or an Observable
  • Auto placement, scroll / resize tracking, keyboard navigation, focus trap, theming and every option come from the core — see its README for details

Install

npm i ngx-tour-guide

Quick start

1. Import the module (and optionally configure it) in AppModule:

import { NgxTourGuideModule } from 'ngx-tour-guide';

@NgModule({
  imports: [
    NgxTourGuideModule.forRoot({
      labels: { next: 'Next', prev: 'Back', skip: 'Skip', done: 'Done' },
      showProgressBar: true,
    }),
  ],
})
export class AppModule {}

Standalone apps (Angular 14+):

bootstrapApplication(App, { providers: [provideTourGuide({ showProgressBar: true })] });

@Component({ imports: [NgxTourGuideModule], ... })
export class App {}

2. Mark elements with the tourStep directive and start the tour:

import { TourService } from 'ngx-tour-guide';

@Component({
  template: `
    <input tourStep="search" tourTitle="Search" tourContent="Find anything." tourOrder="1" />
    <button tourStep="save" tourTitle="Save" tourContent="Don't forget to <b>save</b>." tourOrder="2" tourPlacement="top">
      Save
    </button>

    <button (click)="tour.start()">Start tour</button>
  `,
})
export class PageComponent {
  constructor(public tour: TourService) {}
}

tour.start() collects every tourStep on the page, sorts them by tourOrder and shows the first step. The overlay is created by the core; nothing else goes into templates.

Directive inputs

| Input | Type | Description | | ---------------------- | --------------------------------------- | ---------------------------------------------------- | | tourStep | string | Step id (required) | | tourTitle | string | | | tourContent | string | Text or HTML (sanitized) | | tourTemplate | TemplateRef<TourStepTemplateContext> | Custom content, replaces tourContent | | tourOrder | number \| string | Sort order, default 0 (stable, then DOM order) | | tourPlacement | 'top' \| 'bottom' \| 'left' \| 'right' \| 'auto' | Default from config (auto) | | tourName | string | Group steps into separate tours (start('name')) | | tourPadding | number \| string | Space around the element, px | | tourAllowInteraction | boolean | Let the user click / type in the highlighted element |

Programmatic tours

tour.start([
  { id: 'welcome', template: this.welcomeTpl },                // no target -> centered
  { id: 'search', allowInteraction: true },                    // uses the tourStep="search" directive
  {
    id: 'profile',
    title: 'Your profile',
    content: 'Lives inside the account menu.',
    target: '#menu-profile',                                   // CSS selector, element or () => element
    placement: 'left',
    beforeShow: () => this.menuOpen = true,                    // may return a Promise
    afterHide: () => this.menuOpen = false,
  },
  {
    id: 'orders',
    target: '#orders-table',
    beforeShow: () => this.router.navigate(['/orders']),
    waitForTarget: 3000,                                       // poll up to 3 s for the element
  },
]);

How the target is resolved for a step:

  1. target (selector / element / function), polled up to waitForTarget ms;
  2. otherwise the tourStep directive with the same id;
  3. otherwise the popover is centered on screen.

Explicit steps are merged with the matching directive, so { id: 'search' } inherits the title / content / placement declared in the template.

TourService

| Member | Description | | ------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | | start(stepsOrName?, { startAt?, once?, ifBusy? }) | start() default tour, start('name') named tour, start([...]) explicit. Resolves true when it opened | | startWhen(condition, stepsOrName?, options?) | Starts once condition turns true (a getter or an Observable); returns a cancel function | | hasSeen(once) / markSeen(once) / forgetSeen(once) | Read and write the once flags directly | | next() / prev() / goTo(i) / end(reason?) | Navigation. end reasons: 'complete' \| 'skip' \| 'programmatic' | | state$, isActive$, currentStep$ | Observables (state$ emits a TourState snapshot on every change) | | isActive, currentStep, currentIndex, currentTarget, steps, total, isFirst, isLast | Synchronous getters | | events | Observable<TourEvent> — start, step, end | | core | The underlying tour-guide-core Tour instance |

Call start() after the view exists (a click handler, ngAfterViewInit, ...): directives register themselves in ngOnInit.

Show a tour only once

tour.start(steps, { once: 'onboarding' });

It runs the first time and is skipped afterwards. The flag is written when the person ends the tour — finishing it, closing it or skipping it. Ending it from code (end(), a route change, a component being destroyed) does not count, so nobody loses an onboarding they never got to read.

Bump version when the content changes and everyone sees the new tour once:

tour.start(steps, { once: { key: 'onboarding', version: 2 } });

hasSeen() / markSeen() / forgetSeen() read and write those flags directly: a "Replay the tour" menu item calls forgetSeen before start, a "Don't show me tips" setting calls markSeen.

Flags live in localStorage by default — that is per browser, not per account. See Configuration for a store of your own.

Wait for the screen to be ready

waitForTarget waits a moment for an element. startWhen waits — for as long as it takes — for the person: the detail pane that only exists once a row is picked, the save bar that appears once there are unsaved changes. Steps pointing at those have nothing to highlight while the tour runs on page load, so they fall back to a popover centered on an empty screen. Split the tour into stages instead, one per condition:

export class OrdersComponent implements OnInit, OnDestroy {
  private cancelTour: () => void = () => {};

  constructor(private tour: TourService) {}

  ngOnInit(): void {
    this.cancelTour = this.tour.startWhen(
      () => !!this.selectedOrder, // a getter, checked every `pollInterval` ms
      () => this.detailSteps(),
      { once: { key: 'orders.detail', version: 2 }, delay: 300 },
    );
  }

  ngOnDestroy(): void {
    this.cancelTour();
  }
}

Pass an Observable instead and nothing is polled — the app already says when the value changed:

this.cancelTour = this.tour.startWhen(this.selectedOrder$, () => this.detailSteps());

Anything observable works: a store selector, form.valueChanges, a filtered router.events. Apps on Angular 16+ can drive it from a signal either way:

// an observable view of the signal — still no polling
this.cancelTour = this.tour.startWhen(toObservable(this.selected), () => this.detailSteps());

// or read the signal in a getter, which is polled
this.cancelTour = this.tour.startWhen(() => !!this.selected(), () => this.detailSteps());

Two things to get right:

  • Call the returned function in ngOnDestroy. A wait without a timeout otherwise outlives the component that started it.
  • Pass the steps as a function. It runs when the tour finally opens, so labels are translated with the current language and a TemplateRef is read while the view that owns it is still alive — one captured up front may already be destroyed by then.

| Option | Default | Description | | --------------------------- | ------- | ---------------------------------------------------------------------------------- | | delay | 0 | Wait this long after the condition turns true, letting the app render | | pollInterval | 200 | How often a getter condition is checked. Ignored for an Observable | | timeout | 0 | Give up after this many ms; 0 waits indefinitely | | stillValid | — | Checked again right before opening; false cancels this attempt and keeps waiting | | once, ifBusy, startAt | — | Same as start() |

A tour scheduled this way queues behind one that is already running (ifBusy: 'queue'), so an earlier stage is never cut off mid-sentence. start() keeps its old behaviour and restarts; 'skip' gives up instead.

Custom content

<ng-template #welcomeTpl let-step let-index="index" let-total="total" let-tour="tour">
  <h3>Welcome!</h3>
  <p>Step {{ index + 1 }} of {{ total }}</p>
  <button (click)="tour.goTo(total - 1)">Jump to the end</button>
</ng-template>

The context is { $implicit: step, step, index, total, tour }.

Configuration (forRoot / provideTourGuide)

| Option | Default | Description | | ---------------------- | ---------------------- | ------------------------------------------------- | | labels | English | next, prev, skip, done, close, progress(current, total) | | placement | 'auto' | bottom → top → right → left, first side that fits | | padding | 8 | Space around the highlighted element, px | | spotlightRadius | 8 | Corner radius of the hole, px | | backdropColor | rgba(0, 0, 0, 0.55) | | | popoverWidth | 340 | Max width, px | | zIndex | 10000 | | | keyboard | true | ← → navigate, Esc closes | | closeOnBackdropClick | false | | | showPrev / showSkip / showClose | true | | | showProgress | true | "1 / 5" counter | | showProgressBar | false | | | allowInteraction | false | Default for TourStep.allowInteraction | | scrollIntoView | true | Scroll the target into view before showing: centered, or top-aligned when taller than the viewport, no scroll when already visible. Use CSS scroll-margin-top on targets hidden by sticky headers | | scrollBehavior | 'smooth' | | | waitForTarget | 1000 | Default polling timeout, ms | | seenStore | localStorage | Where once flags are kept. null disables once. Every access is guarded, so it is a no-op during SSR and when site data is blocked | | seenPrefix | 'tour-guide:' | Prefix for keys written to seenStore |

localStorage is per browser, not per account: two people sharing a machine means the second one never sees the onboarding, and nothing follows anyone to another device. Keep the flags where the rest of the user's profile lives:

provideTourGuide({
  labels: { next: 'Next', prev: 'Back', skip: 'Skip', done: 'Done' },
  seenStore: {
    has: (key) => profile.seenTours.indexOf(key) !== -1,
    mark: (key) => profile.markTourSeen(key), // fire-and-forget; return value ignored
    forget: (key) => profile.forgetTour(key),
  },
});

provideTourGuide() takes a plain value, so when the store needs injected services, provide the token itself:

providers: [
  {
    provide: TOUR_CONFIG,
    useFactory: (account: AccountService) => ({ seenStore: account.tourStore }),
    deps: [AccountService],
  },
];

createLocalStorageSeenStore(getStorage?), createMemorySeenStore() (per page load) and createNoopSeenStore() (never remembers) are re-exported from this package for the common cases, along with the TourSeenStore type and seenKey().

Theming

All colors and sizes are CSS custom properties on .tour-overlay (global styles, e.g. styles.scss):

.tour-overlay {
  --tour-bg: #1f2937;
  --tour-fg: #f9fafb;
  --tour-muted: #9ca3af;
  --tour-border: rgba(255, 255, 255, 0.15);
  --tour-primary: #22c55e;
  --tour-primary-fg: #052e16;
  --tour-radius: 16px;
  --tour-shadow: 0 20px 60px rgba(0, 0, 0, 0.5);
  --tour-ring: rgba(255, 255, 255, 0.3);
  --tour-font: inherit;
}

Class names are stable (tour-popover, tour-title, tour-content, tour-btn, tour-btn--primary, tour-arrow, tour-spotlight, tour-backdrop, ...) if you need deeper overrides.

Compatibility

| Angular | Status | | ------- | ------------------------------------------------------------- | | 12 – 22 | Supported (compiled with Angular 12, Ivy partial; depends on tour-guide-core) | | ≤ 11 | Not supported: those versions need View Engine packages, which Angular 16+ can no longer consume |

Browsers: clip-path: path() is used for the spotlight — Chrome/Edge 88+, Firefox 97+, Safari 13.1+ (no IE11).

License

MIT