https://github.com/HomelessCoder/ng-beacon
A lightweight, signal-native spotlight tour library for Angular 19+ zoneless applications. Zero external dependencies beyond Angular itself.
https://github.com/HomelessCoder/ng-beacon
Last synced: about 21 hours ago
JSON representation
A lightweight, signal-native spotlight tour library for Angular 19+ zoneless applications. Zero external dependencies beyond Angular itself.
- Host: GitHub
- URL: https://github.com/HomelessCoder/ng-beacon
- Owner: HomelessCoder
- License: mit
- Created: 2026-03-08T23:14:39.000Z (20 days ago)
- Default Branch: main
- Last Pushed: 2026-03-17T20:59:46.000Z (11 days ago)
- Last Synced: 2026-03-18T10:10:01.536Z (10 days ago)
- Language: TypeScript
- Homepage:
- Size: 2.88 MB
- Stars: 1
- Watchers: 0
- Forks: 0
- Open Issues: 0
-
Metadata Files:
- Readme: README.md
- License: LICENSE
Awesome Lists containing this project
- fucking-awesome-angular - ng-beacon - Lightweight guided-tour library for Angular 19+ with signals and zoneless-compatible rendering. (Third Party Components / Onboarding and Product Tours)
- awesome-angular - ng-beacon - Lightweight guided-tour library for Angular 19+ with signals and zoneless-compatible rendering. (Third Party Components / Onboarding and Product Tours)
README
# ng-beacon
Lightweight guided-tour library for Angular 19+ with Angular Signals and zoneless-compatible rendering.
SVG spotlight overlays, keyboard navigation, and lightweight i18n hooks with zero runtime dependencies beyond Angular.

[](https://github.com/HomelessCoder/ng-beacon/actions/workflows/ci.yml)
[](https://www.npmjs.com/package/ng-beacon)
[](LICENSE)
## Features
- **Signal-based** — reactive state via Angular Signals, fully OnPush / zoneless compatible
- **SVG spotlight** — smooth cutout mask highlights the target element
- **Accessible focus handling** — focus moves into the tooltip and is restored on close
- **Keyboard support** — Escape closes, ArrowLeft goes back, ArrowRight advances
- **i18n ready** — plug in any translation function (ngx-translate, Transloco, etc.)
- **Theming** — CSS custom properties for colors, radius, shadow, width
- **Tiny footprint** — no Material, no CDK, no extra runtime deps
## Stackblitz Demo
https://stackblitz.com/edit/stackblitz-starters-midtdjmx
## Quick Start
### 1. Install
```bash
npm install ng-beacon
```
### 2. Provide
```ts
// app.config.ts
import { provideBeacon } from 'ng-beacon';
export const appConfig = {
providers: [
provideBeacon(),
],
};
```
### 3. Add the overlay
```html
@if (beaconService.isActive()) {
}
```
```ts
// app.component.ts
import { BeaconOverlay, BeaconService } from 'ng-beacon';
@Component({
imports: [BeaconOverlay],
// ...
})
export class AppComponent {
readonly beaconService = inject(BeaconService);
}
```
### 4. Define steps
```ts
import { BeaconStep } from 'ng-beacon';
export const MY_TOUR: BeaconStep[] = [
{
id: 'welcome',
title: 'Welcome!',
content: 'Let me show you around.',
position: 'center',
showWithoutTarget: true,
},
{
id: 'sidebar',
title: 'Sidebar',
content: 'Navigate between sections here.',
position: 'end',
selector: '[data-tour="sidebar"]',
},
];
```
### 5. Start the tour
```ts
this.beaconService.start(MY_TOUR);
```
## Component-Scoped Step Registration
Register steps that are only available while a component is alive:
```ts
import { registerTourSteps } from 'ng-beacon';
@Component({ /* ... */ })
export class DashboardComponent {
private readonly _tour = registerTourSteps(DASHBOARD_STEPS);
}
```
Then start a context-aware tour — steps from destroyed components are automatically pruned:
```ts
this.beaconService.startContextTour();
```
## Translation (i18n)
```ts
import { provideBeacon, provideBeaconTranslateFn } from 'ng-beacon';
providers: [
provideBeacon({
labels: { close: 'tour.close', nextStep: 'tour.next', prevStep: 'tour.back' },
}),
provideBeaconTranslateFn(() => {
const translate = inject(TranslateService);
return (key: string) => translate.instant(key);
}),
]
```
## Optional Router Integration
If your app uses Angular Router and you want tours to close after route changes, subscribe to `NavigationEnd` in app-level code and call `stop()`:
```ts
import { Component, DestroyRef, inject } from '@angular/core';
import { takeUntilDestroyed } from '@angular/core/rxjs-interop';
import { NavigationEnd, Router } from '@angular/router';
import { filter } from 'rxjs';
import { BeaconService } from 'ng-beacon';
@Component({
selector: 'app-root',
template: ``,
})
export class AppComponent {
private readonly router = inject(Router);
private readonly destroyRef = inject(DestroyRef);
private readonly beaconService = inject(BeaconService);
constructor() {
this.router.events
.pipe(
filter((event): event is NavigationEnd => event instanceof NavigationEnd),
takeUntilDestroyed(this.destroyRef),
)
.subscribe(() => {
if (this.beaconService.isActive()) {
this.beaconService.stop();
}
});
}
}
```
## Theming
Override CSS custom properties on `beacon-overlay` or any ancestor:
```css
beacon-overlay {
--beacon-bg: #1e1e2e;
--beacon-text: #cdd6f4;
--beacon-primary: #89b4fa;
--beacon-primary-hover: #74c7ec;
--beacon-radius: 16px;
--beacon-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
--beacon-width: 360px;
}
```
## API
### `BeaconService`
| Signal / Method | Description |
|---|---|
| `isActive()` | Whether a tour is running |
| `currentStep()` | Current `BeaconStep` or `null` |
| `currentStepIndex()` | Zero-based index or `null` |
| `totalSteps()` | Number of steps (0 when idle) |
| `isFirstStep()` / `isLastStep()` | Position booleans |
| `start(steps)` | Start a tour with explicit steps (snapshot — not reactive to registry changes) |
| `startContextTour()` | Start a tour from all registered context steps (reactive — steps are pruned when components are destroyed) |
| `next()` / `prev()` | Navigate between steps; `next()` stops on the last step and `prev()` stays on the first step |
| `stop()` | End the tour |
| `recalculate()` | Re-evaluate step visibility and rebuild the active context tour, preserving the current position by step `id`. No-op for `start()` tours. |
| `finished()` | `Signal` — emits when a tour is completed (user reached the last step) |
| `dismissed()` | `Signal` — emits when a tour is closed early (close button, Escape, click outside, or programmatic `stop()`) |
| `registerContextSteps(steps)` | Add steps to the registry |
| `unregisterContextSteps(steps)` | Remove steps from the registry |
### `BeaconStep`
```ts
interface BeaconStep {
id: string;
title: string;
content: string;
position: 'above' | 'below' | 'start' | 'end' | 'center';
selector?: string;
showWithoutTarget?: boolean;
}
```
### `BeaconTourEvent`
```ts
interface BeaconTourEvent {
step: BeaconStep; // the step that was active when the event fired
stepIndex: number; // zero-based index
totalSteps: number; // total steps in the tour
}
```
## Events
`BeaconService` exposes two signals for tracking tour lifecycle:
- **`finished`** — emits when the user completes all steps (clicks next on the last step)
- **`dismissed`** — emits when the tour is closed early (close button, `Escape`, click outside, or programmatic `stop()`)
Both are `Signal`, starting as `null`. Each emission is a new object reference, so `effect()` fires every time.
```ts
import { effect, inject } from '@angular/core';
import { BeaconService } from 'ng-beacon';
export class AppComponent {
private readonly beaconService = inject(BeaconService);
constructor() {
effect(() => {
const event = this.beaconService.finished();
if (event) {
localStorage.setItem('tour-completed', 'true');
}
});
effect(() => {
const event = this.beaconService.dismissed();
if (event) {
console.log(`Tour dismissed at step ${event.stepIndex + 1}/${event.totalSteps}`);
}
});
}
}
```
## Keyboard Support
| Key | Action |
|---|---|
| `Escape` | Stop the tour |
| `ArrowLeft` | Go to the previous step |
| `ArrowRight` | Go to the next step |
## Development
```bash
npm install
npm test # run tests (ChromeHeadless, coverage enforced)
npm run build # build the library
```
## License
[MIT](LICENSE)