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

@korsolutions/guidon

v1.1.2

Published

A cross-platform walkthrough/onboarding component library for React Native with web support. Features spotlight effects, customizable tooltips, and flexible persistence options.

Readme

Guidon

A lightweight, cross-platform walkthrough/onboarding library for React Native with web support. Features spotlight effects, draggable tooltips, and flexible persistence options.

Features

  • Zero state management dependencies - Built with React's useSyncExternalStore
  • New Architecture ready - Full support for React Native's Fabric renderer
  • Cross-platform - iOS, Android, and Web (React Native Web)
  • Draggable tooltips - Users can reposition tooltips by dragging
  • Multi-screen tours - Guides pause when targets aren't mounted, resume when user navigates
  • Floating steps - Create announcement/welcome screens without targeting elements
  • Flexible persistence - Save progress to localStorage, AsyncStorage, or your API
  • Customizable - Full theming, custom tooltip renderers, and per-step callbacks

Installation

yarn add @korsolutions/guidon
# or
npm install @korsolutions/guidon

Peer Dependencies

Make sure you have these dependencies installed:

yarn add react react-native react-native-reanimated react-native-safe-area-context react-native-svg

Quick Start

1. Configure Tours (at app root)

import { Guidon, GuidonProvider, GuidonToursConfig } from '@korsolutions/guidon';

// Configure all tours ONCE outside React (stable reference)
const config: GuidonToursConfig  = {
  // Global theme applies to all tours
  theme: {
    primaryColor: '#007AFF',
    backdropOpacity: 0.8,
  },
  animationDuration: 300,

  // Define your tours
  tours: {
    explore: {
      id: 'explore',
      steps: [
        { id: 'search', targetId: 'search-btn', title: 'Search', description: 'Find what you need' },
        { id: 'filter', targetId: 'filter-btn', title: 'Filter', description: 'Narrow results' },
      ],
      onComplete: () => console.log('Explore tour done'),
    },
    profile: {
      id: 'profile',
      steps: [
        { id: 'edit', targetId: 'edit-btn', title: 'Edit Profile', description: 'Update your info' },
      ],
      // Override theme just for this tour
      theme: { primaryColor: '#FF6B6B' },
    },
  },
} satisfies GuidonToursConfig;

Guidon.configureTours(config);

// In your app root
export default function App() {
  return (
    <QueryClientProvider client={queryClient}>
      <GuidonProvider>
        <Navigation />
      </GuidonProvider>
    </QueryClientProvider>
  );
}

2. Start Tours from Screens

import { useEffect } from 'react';
import { Guidon, useGuidon } from '@korsolutions/guidon';

function ExploreScreen() {
  const { register } = useGuidon();

  useEffect(() => {
    Guidon.start('explore');  // Start this screen's tour
    return () => Guidon.stop(); // Stop when leaving screen
  }, []);

  return (
    <View>
      <Button ref={register('search-btn')} title="Search" />
      <Button ref={register('filter-btn')} title="Filter" />
    </View>
  );
}

Alternative: GuidonTarget Component

import { GuidonTarget } from '@korsolutions/guidon';

function ExploreScreen() {
  return (
    <View>
      <GuidonTarget targetId="search-btn">
        <Button title="Search" />
      </GuidonTarget>
      <GuidonTarget targetId="filter-btn">
        <Button title="Filter" />
      </GuidonTarget>
    </View>
  );
}

Floating Steps (No Target)

Create steps without a target element for welcome screens or announcements:

const config = {
  tours: {
    welcome: {
      id: 'welcome',
      steps: [
        {
          id: 'intro',
          // No targetId - this is a floating step!
          title: 'Welcome to Our App!',
          description: 'Let us show you around.',
          floatingPosition: 'center', // 'center' | 'top' | 'bottom' | 'top-left' | etc.
        },
        {
          id: 'feature-1',
          targetId: 'some-button',
          title: 'First Feature',
          description: 'This highlights an element.',
        },
      ],
    },
  },
} satisfies GuidonToursConfig;

Draggable Tooltips

Enable draggable tooltips so users can reposition them if they're blocking content:

const config = {
  theme: {
    tooltipDraggable: true,
    dragHintText: 'Drag to reposition', // Optional custom hint
  },
  tours: {
    // ...
  },
} satisfies GuidonToursConfig;

When enabled:

  • A hint ("Drag to move") appears on the first step
  • Users can drag the tooltip to any position on screen
  • Position resets when advancing to the next step
  • Works on both mobile (touch) and web (mouse)

Multi-Screen Tours

Tours can span multiple screens. When a step targets an element that isn't mounted yet, the guide shows a waiting state until the user navigates to the correct screen:

const config = {
  // Auto-skip after 10 seconds if target doesn't appear
  defaultWaitTimeout: 10000,
  tours: {
    onboarding: {
      id: 'onboarding',
      steps: [
        { id: 'home', targetId: 'home-button', title: 'Welcome', description: 'Start here' },
        {
          id: 'settings',
          targetId: 'settings-toggle', // On Settings screen
          title: 'Settings',
          description: 'Configure your preferences',
          waitingMessage: 'Navigate to Settings to continue...', // Custom waiting text
        },
      ],
    },
  },
} satisfies GuidonToursConfig;

The guide will:

  1. Show a loading indicator when the target element isn't found
  2. Display the waitingMessage (or default "Navigate to continue...")
  3. Automatically resume when the user navigates and the target mounts
  4. Optionally auto-skip after defaultWaitTimeout milliseconds

Persistence

The library supports flexible persistence through adapters. You can save guidon progress to local storage, AsyncStorage, or your backend API.

Using Built-in Adapters

import {
  GuidonProvider,
  createLocalStorageAdapter,
  createAsyncStorageAdapter,
} from '@korsolutions/guidon';
import AsyncStorage from '@react-native-async-storage/async-storage';

// For web (localStorage)
const webAdapter = createLocalStorageAdapter();

// For React Native (AsyncStorage)
const nativeAdapter = createAsyncStorageAdapter(AsyncStorage);

function App() {
  return (
    <GuidonProvider persistenceAdapter={nativeAdapter}>
      <YourApp />
    </GuidonProvider>
  );
}

Creating a Custom API Adapter

import { createApiAdapter } from '@korsolutions/guidon';

const apiAdapter = createApiAdapter({
  loadProgress: async (guidonId) => {
    const response = await fetch(`/api/guidon/${guidonId}`);
    if (!response.ok) return null;
    return response.json();
  },
  saveProgress: async (progress) => {
    await fetch(`/api/guidon/${progress.guidonId}`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify(progress),
    });
  },
  clearProgress: async (guidonId) => {
    await fetch(`/api/guidon/${guidonId}`, {
      method: 'DELETE',
    });
  },
});

Combining Multiple Adapters

import { createCompositeAdapter, createLocalStorageAdapter } from '@korsolutions/guidon';

// Save to both local storage and API
const compositeAdapter = createCompositeAdapter([
  createLocalStorageAdapter(),
  apiAdapter,
]);

API Reference

Guidon API (Imperative)

Use outside React components or for programmatic control:

import { Guidon } from '@korsolutions/guidon';

// Configuration
Guidon.configureTours(config);   // Configure all tours

// Control
Guidon.start('explore');         // Start a specific tour by ID
Guidon.stop();                   // Stop current tour without completing
Guidon.next();                   // Go to next step
Guidon.previous();               // Go to previous step
Guidon.goToStep(2);              // Jump to specific step
Guidon.skip();                   // Skip the tour
Guidon.complete();               // Complete the tour

// State queries
Guidon.isActive();               // Check if a tour is running
Guidon.isCompleted();            // Check if tour completed
Guidon.getActiveTourId();        // Get current tour ID
Guidon.getTours();               // Get all configured tours
Guidon.getCurrentStepIndex();    // Get current step index
Guidon.getCurrentStep();         // Get current step object
Guidon.getSteps();               // Get all steps of active tour

// Reset
Guidon.reset();                  // Reset to initial state

GuidonProvider Props

| Prop | Type | Default | Description | |------|------|---------|-------------| | autoStart | string | - | Tour ID to auto-start when mounted | | shouldStart | () => boolean \| Promise<boolean> | - | Custom condition for starting | | persistenceAdapter | GuidonPersistenceAdapter | - | Adapter for saving progress | | portalComponent | React.ComponentType | - | Custom portal for overlay rendering | | renderTooltip | (props) => ReactNode | - | Custom tooltip renderer | | tooltipLabels | object | - | Customize button labels | | onBackdropPress | () => void | - | Called when backdrop is pressed |

GuidonToursConfig

import { GuidonToursConfig } from '@korsolutions/guidon';

const config = {
  // Global settings apply to all tours
  theme: {
    primaryColor: '#007AFF',
    backdropOpacity: 0.8,
  },
  animationDuration: 300,

  // Define tours
  tours: {
    explore: {
      id: 'explore',
      steps: [{ id: 's1', title: 'Welcome', description: '...' }],
      onComplete: () => console.log('Done'),
    },
    profile: {
      id: 'profile',
      steps: [...],
      theme: { primaryColor: '#FF0000' }, // Override for this tour only
    },
  },
} satisfies GuidonToursConfig;

GuidonStep

interface GuidonStep {
  id: string;                    // Unique step identifier
  targetId?: string;             // ID of target element (optional for floating steps)
  title: string;                 // Tooltip title
  description: string;           // Tooltip description
  tooltipPosition?: 'top' | 'bottom' | 'left' | 'right' | 'auto';
  floatingPosition?: 'center' | 'top' | 'bottom' | 'top-left' | 'top-right' | 'bottom-left' | 'bottom-right';
  customContent?: ReactNode;     // Additional content in tooltip
  onStepEnter?: () => void;      // Called when step becomes active
  onStepExit?: () => void;       // Called when leaving step
  waitingMessage?: string;       // Message while waiting for target to mount
  waitTimeout?: number;          // Auto-skip timeout (ms) if target doesn't mount
}

GuidonTheme

interface GuidonTheme {
  backdropColor?: string;         // Overlay color (default: '#000000')
  backdropOpacity?: number;       // Overlay opacity (default: 0.5)
  tooltipBackgroundColor?: string;
  tooltipBorderColor?: string;
  tooltipBorderRadius?: number;
  titleColor?: string;
  descriptionColor?: string;
  primaryColor?: string;          // Button color
  mutedColor?: string;            // Secondary text color
  spotlightBorderRadius?: number; // Spotlight cutout radius
  spotlightPadding?: number;      // Padding around spotlight
  tooltipDraggable?: boolean;     // Enable draggable tooltips (default: false)
  dragHintText?: string;          // Hint text shown on first step (default: 'Drag to move')
}

Controlling the Guidon

Using the Context Hook

import { useGuidonContext } from '@korsolutions/guidon';

function ReplayButton() {
  const { replay, isCompleted, start } = useGuidonContext();

  return (
    <>
      {isCompleted && (
        <Button onPress={() => replay('explore')}>Replay Tutorial</Button>
      )}
      <Button onPress={() => start('explore')}>Start Explore Tour</Button>
    </>
  );
}

Using Hook Selectors

import {
  useGuidonActive,
  useGuidonStep,
  useGuidonProgress,
} from '@korsolutions/guidon';

function GuidonStatus() {
  const isActive = useGuidonActive();
  const currentStep = useGuidonStep();
  const { currentStep: stepNum, totalSteps, percentage } = useGuidonProgress();

  if (!isActive) return null;

  return (
    <Text>
      Step {stepNum} of {totalSteps} ({percentage}%)
    </Text>
  );
}

Custom Tooltip Rendering

<GuidonProvider
  renderTooltip={({ step, currentIndex, totalSteps, onNext, onPrevious, onSkip }) => (
    <View style={styles.customTooltip}>
      <Text style={styles.title}>{step.title}</Text>
      <Text>{step.description}</Text>
      <View style={styles.buttons}>
        <Button onPress={onSkip}>Skip</Button>
        {currentIndex > 0 && <Button onPress={onPrevious}>Back</Button>}
        <Button onPress={onNext}>
          {currentIndex === totalSteps - 1 ? 'Done' : 'Next'}
        </Button>
      </View>
    </View>
  )}
>

Conditional Starting

<GuidonProvider
  autoStart="welcome"
  shouldStart={async () => {
    // Check if user is new
    const user = await getUser();
    return user.isFirstLogin;
  }}
>

Auto-Scroll Behavior

Web

On web, targets automatically scroll into view using the native scrollIntoView() API. You can customize this via theme:

const config = {
  theme: {
    scrollOptions: {
      behavior: 'smooth',  // 'smooth' | 'instant'
      block: 'center',     // 'start' | 'center' | 'end' | 'nearest'
      inline: 'nearest',   // 'start' | 'center' | 'end' | 'nearest'
    },
  },
  tours: { ... },
} satisfies GuidonToursConfig;

Mobile (React Native)

For mobile, use the useGuidonScrollContainer hook to enable auto-scrolling:

import { useRef } from 'react';
import { ScrollView } from 'react-native';
import { useGuidon, useGuidonScrollContainer } from '@korsolutions/guidon';

function MyScreen() {
  const scrollRef = useRef<ScrollView>(null);
  const { register } = useGuidon();

  // Enable auto-scroll when steps change
  useGuidonScrollContainer(scrollRef);

  return (
    <ScrollView ref={scrollRef}>
      <View ref={register('step-1')}>First target</View>
      <View ref={register('step-2')}>Second target</View>
    </ScrollView>
  );
}

Options:

useGuidonScrollContainer(scrollRef, {
  padding: 150,    // Distance from top of viewport (default: 100)
  enabled: false,  // Temporarily disable (default: true)
});

Disabling Scroll Per Step

Disable auto-scroll for individual steps:

{
  id: 'modal-step',
  targetId: 'modal-button',
  title: 'Modal Content',
  description: 'This element is already visible',
  scrollIntoView: false,  // Don't scroll for this step
}

Platform Support

  • iOS (New Architecture / Fabric supported)
  • Android (New Architecture / Fabric supported)
  • Web (React Native Web)

Requirements

  • React Native 0.71+ (for New Architecture support)
  • React 18+ (for useSyncExternalStore)

License

MIT