@math_o_chris/beacon-react
v0.1.0
Published
React wrapper for beacon.js — provider, hook, and declarative target component.
Downloads
16
Maintainers
Readme
@math_o_chris/beacon-react
Thin React wrapper over
beacon.js. Provider,useBeaconhook, declarative<BeaconTarget>component. No runtime dependencies — React and@math_o_chris/beacon-coreare peer deps.
For the broader project overview see the repo root. For the underlying API see the core README.
Install
pnpm add @math_o_chris/beacon-core @math_o_chris/beacon-react react react-domreact ^18 or ^19 (and matching react-dom).
Quickstart
import { BeaconProvider, BeaconTarget, useBeacon } from '@math_o_chris/beacon-react';
export function App() {
return (
<BeaconProvider>
<Launcher />
<BeaconTarget tourId="onboarding" step={0} tooltip={{ title: 'Welcome' }}>
<button id="get-started">Get started</button>
</BeaconTarget>
<BeaconTarget tourId="onboarding" step={1} tooltip={{ body: 'And here is the sidebar.' }}>
<aside id="sidebar">…</aside>
</BeaconTarget>
</BeaconProvider>
);
}
function Launcher() {
const { startTour } = useBeacon();
return <button onClick={() => startTour('onboarding')}>Start tour</button>;
}<BeaconTarget> registers its child element with the provider on mount and unregisters on unmount. startTour('onboarding') walks every registered target, sorts by step, and calls beacon.start() under the hood — so the tour stays in sync with whatever React is rendering at that moment.
API
<BeaconProvider options>
<BeaconProvider options={{ scrollBehavior: 'smooth' }}>{children}</BeaconProvider>Owns the registry of <BeaconTarget> instances and exposes the controller via context. Default options here are merged with per-call options when you invoke startTour() / focus() / start().
<BeaconTarget>
<BeaconTarget
tourId="onboarding"
step={0}
tooltip={{ title: 'Welcome', placement: 'bottom' }}
padding={4}
borderRadius={8}
>
<button>Click me</button>
</BeaconTarget>The component must wrap a single ref-able child element. It captures a ref to that element (composing with any existing child ref), registers { element, step, tooltip, padding, borderRadius } with the provider, and stamps data-beacon-* attributes on the child so it's also discoverable by the imperative register() scanner — handy for hybrid React + vanilla pages.
useBeacon()
const {
start, // start an explicit tour object
startTour, // build and start a tour from <BeaconTarget> registrations
focus, // single-step focus
stop, // stop active controller
next, prev,
goTo, // navigate
controller, // the active BeaconController, or null
currentStep, // 0-based, reactive
stepCount,
isActive, // !!controller
} = useBeacon();The hook subscribes to the active controller's step / stop / finish events and re-renders consumers when state changes. useBeacon throws if used outside a <BeaconProvider>.
Re-exported types
@math_o_chris/beacon-react re-exports the core's type surface so you don't need to also import from @math_o_chris/beacon-core for typings:
import type {
BeaconController, BeaconTour, BeaconStep, BeaconOptions, BeaconTheme,
TooltipConfig, Placement, NavPosition, NavProgress,
} from '@math_o_chris/beacon-react';Patterns
Hybrid: react components + vanilla starter
Mount <BeaconProvider> for the React side, but also call beacon.register() once in your app entry. The data-attribute scanner will pick up <BeaconTarget>s (which mirror their props as data-beacon-* attributes), so any <button data-beacon-trigger="onboarding"> outside the React tree can launch the same tour without a hook.
Async lifecycle hooks
Pass async onBeforeShow / onAfterShow / onBeforeHide to await data-fetching or animation:
<BeaconTarget tourId="onboarding" step={2} tooltip={{ title: 'Reports' }}>
<ReportsButton />
</BeaconTarget>startTour('onboarding', {
// ...
// each `BeaconStep` accepts onBeforeShow/onAfterShow/onBeforeHide;
// for declarative usage, pass them through buildTour or write a custom wrapper.
});For the most flexible flow combine <BeaconTarget> registrations with imperative start() so you can attach hooks per step.
TypeScript
The step={0} literal type is widened to number automatically; both step={0} and step={someVar} work.
If you forward a ref to your custom child component, make sure it's forwardRef-compatible (or pass a real DOM element). See docs/recipes.md for the standard React-ref-forwarding patterns.
License
MIT — see LICENSE.
