Torchlit docs

Torchlit

Shadow DOM-aware guided tours for any web framework. Lightweight, framework-agnostic, and built on Lit web components.

$ npm install torchlit lit
Bundle size
~9 KB
Dependencies
Lit only
Frameworks
Any
Shadow DOM
Yes
Shadow DOM Traversal
Finds tour targets inside shadow roots — something Shepherd.js and Intro.js can't do. Uses deepQuery to traverse any depth.
Framework Agnostic
Web Components work in React, Vue, Angular, Svelte, or vanilla JS — zero adapters needed.
CSS Custom Properties
Theme with CSS variables. Dark mode, brand colors — the overlay adapts automatically.
Tiny Footprint
~9 KB gzipped (with Lit peer). Compare to Shepherd (~50 KB with Popper).
Accessible by Default
ARIA role="dialog", focus trapping, keyboard navigation, and aria-live announcements for screen readers.
Smart Auto-Positioning
Tooltips flip automatically when the preferred side clips the viewport. Arrow tracks the target center.
MutationObserver Targets
No fixed timeouts — watches the DOM for lazy-loaded elements to appear. Reliable in SPAs with async routes.

Quick Start

Three steps: create a service, register a tour, mount the overlay.

import { createTourService } from 'torchlit';
import 'torchlit/overlay';

const tours = createTourService();

tours.register({
  id: 'welcome',
  name: 'Welcome',
  trigger: 'first-visit',
  steps: [
    { target: 'sidebar', title: 'Navigation', message: 'Use the sidebar to get around.', placement: 'right' },
    { target: 'search',  title: 'Search',     message: 'Find anything instantly.',       placement: 'bottom' },
  ],
});

const overlay = document.querySelector('torchlit-overlay');
overlay.service = tours;

if (tours.shouldAutoStart('welcome')) {
  tours.start('welcome');
}

Getting Started

A step-by-step guide to adding guided tours to your application.

1
Install

Add Torchlit and its Lit peer dependency.

npm install torchlit lit
2
Import & Create a Service

The TourService manages all tour state — registration, navigation, and persistence. The overlay is a separate import so you can tree-shake if needed.

import { createTourService } from 'torchlit';
import 'torchlit/overlay';  // registers <torchlit-overlay>

const tours = createTourService({
  storageKey: 'my-app-tours',   // localStorage key for state
  spotlightPadding: 12,         // px around highlighted element
});

For headless usage, import from torchlit/service — that entrypoint is DOM-free. If your TypeScript code reads snapshot.step directly, type the service with createTourService<TourStep>().

3
Mark Your Targets

Add data-tour-id attributes to any element you want to spotlight. Works in light DOM and shadow DOM — Torchlit traverses shadow roots automatically.

<nav data-tour-id="sidebar">…</nav>
<input data-tour-id="search" placeholder="Search…" />
<my-web-component>
  <!-- targets inside shadow roots work too -->
</my-web-component>
4
Define a Tour

A tour is an object with an id, a trigger, and an array of steps. Each step targets an element and shows a tooltip.

tours.register({
  id: 'onboarding',
  name: 'Welcome Tour',
  trigger: 'first-visit',   // or 'manual'
  steps: [
    {
      target: '_none_',        // centered card, no spotlight
      title: 'Welcome!',
      message: 'Let us show you around.',
      placement: 'bottom',
    },
    {
      target: 'sidebar',
      title: 'Navigation',
      message: 'Browse sections from the sidebar.',
      placement: 'right',
    },
    {
      target: 'search',
      title: 'Search',
      message: 'Find anything with the search bar.',
      placement: 'bottom',
    },
  ],
  onComplete: () => console.log('Done!'),
  onSkip: () => console.log('Skipped'),
});
5
Mount the Overlay & Start

Drop the <torchlit-overlay> element anywhere in your page. Assign the service and start the tour.

<torchlit-overlay></torchlit-overlay>

<script type="module">
  const overlay = document.querySelector('torchlit-overlay');
  overlay.service = tours;

  // Auto-start on first visit
  if (tours.shouldAutoStart('onboarding')) {
    tours.start('onboarding');
  }
</script>

Route Changes

Tours can span multiple views. Add a route property to a step and use beforeShow to navigate. The overlay emits a tour-route-change event so your app can react.

// Step that switches pages
{
  target: 'dashboard-chart',
  title: 'Dashboard',
  message: 'Check out the live charts.',
  placement: 'top',
  route: 'dashboard',
  beforeShow: () => router.push('/dashboard'),
}

// Listen for route changes from the overlay
overlay.addEventListener('tour-route-change', (e) => {
  router.push(e.detail.route);
});

Contextual Help Pattern

Register short manual tours for each page, then start the right one based on the current route. This is exactly what this docs site does — click the ? button to try it.

const helpMap = { home: 'home-help', settings: 'settings-help' };

helpButton.addEventListener('click', () => {
  const tourId = helpMap[currentPage];
  if (tourId) tours.start(tourId);
});

Recipes

Ready-to-copy patterns for common use cases and framework integrations.

Framework Integration

Torchlit is a web component — it works with any framework. Here's how to wire it into the most popular ones.

If your framework code reads snapshot.step directly in TypeScript, create the service with createTourService<TourStep>() so the step is strongly typed.

React

Create the service once in a hook or context provider. Use a ref for the overlay and assign .service in useEffect.

import { useEffect, useRef } from 'react';
import { createTourService } from 'torchlit';
import 'torchlit/overlay';

const tours = createTourService({ storageKey: 'my-app-tours' });

tours.register({
  id: 'welcome',
  name: 'Welcome',
  trigger: 'first-visit',
  steps: [
    { target: 'sidebar', title: 'Navigation', message: 'Browse from here.', placement: 'right' },
    { target: 'search',  title: 'Search',     message: 'Find anything.',    placement: 'bottom' },
  ],
});

export function App() {
  const overlayRef = useRef<HTMLElement>(null);

  useEffect(() => {
    const el = overlayRef.current;
    if (el) el.service = tours;

    if (tours.shouldAutoStart('welcome')) {
      setTimeout(() => tours.start('welcome'), 500);
    }
  }, []);

  return (
    <>
      {/* your app JSX */}
      <torchlit-overlay ref={overlayRef} />
    </>
  );
}
Tip: For shared access across components, expose tours via React Context or a module-level singleton.

Vue 3

Use a composable or create the service at the app level. Assign via a template ref in onMounted.

<script setup lang="ts">
import { ref, onMounted } from 'vue';
import { createTourService } from 'torchlit';
import 'torchlit/overlay';

const tours = createTourService({ storageKey: 'my-app-tours' });
const overlayRef = ref<HTMLElement | null>(null);

tours.register({
  id: 'welcome',
  name: 'Welcome',
  trigger: 'first-visit',
  steps: [
    { target: 'sidebar', title: 'Navigation', message: 'Browse from here.', placement: 'right' },
    { target: 'search',  title: 'Search',     message: 'Find anything.',    placement: 'bottom' },
  ],
});

onMounted(() => {
  if (overlayRef.value) overlayRef.value.service = tours;

  if (tours.shouldAutoStart('welcome')) {
    setTimeout(() => tours.start('welcome'), 500);
  }
});
</script>

<template>
  <!-- your app template -->
  <torchlit-overlay ref="overlayRef" />
</template>

Svelte

Bind the overlay element with bind:this and wire it up in onMount.

<script>
  import { onMount } from 'svelte';
  import { createTourService } from 'torchlit';
  import 'torchlit/overlay';

  const tours = createTourService({ storageKey: 'my-app-tours' });
  let overlay;

  tours.register({
    id: 'welcome',
    name: 'Welcome',
    trigger: 'first-visit',
    steps: [
      { target: 'sidebar', title: 'Navigation', message: 'Browse from here.', placement: 'right' },
      { target: 'search',  title: 'Search',     message: 'Find anything.',    placement: 'bottom' },
    ],
  });

  onMount(() => {
    overlay.service = tours;
    if (tours.shouldAutoStart('welcome')) {
      setTimeout(() => tours.start('welcome'), 500);
    }
  });
</script>

<!-- your app markup -->
<torchlit-overlay bind:this={overlay} />

Plain JS / TS

No framework needed — just a <script type="module"> tag.

<torchlit-overlay></torchlit-overlay>

<script type="module">
  import { createTourService, TorchlitOverlay } from 'torchlit';

  if (!customElements.get('torchlit-overlay')) {
    customElements.define('torchlit-overlay', TorchlitOverlay);
  }

  const tours = createTourService();
  const overlay = document.querySelector('torchlit-overlay');
  overlay.service = tours;

  tours.register({
    id: 'welcome', name: 'Welcome', trigger: 'first-visit',
    steps: [
      { target: 'sidebar', title: 'Navigation', message: 'Browse.', placement: 'right' },
    ],
  });

  if (tours.shouldAutoStart('welcome')) tours.start('welcome');
</script>

SPA Multi-View Tours

In single-page apps, tours can span multiple views without losing state. Use beforeShow to navigate and route to emit a tour-route-change event.

tours.register({
  id: 'full-tour',
  name: 'App Tour',
  trigger: 'first-visit',
  steps: [
    {
      target: 'dashboard-stats',
      title: 'Dashboard',
      message: 'Your key metrics at a glance.',
      placement: 'bottom',
      beforeShow: () => router.push('/dashboard'),
    },
    {
      target: 'analytics-chart',
      title: 'Analytics',
      message: 'The tour followed you to a new page!',
      placement: 'top',
      route: 'analytics',
      beforeShow: () => router.push('/analytics'),
    },
    {
      target: 'settings-panel',
      title: 'Settings',
      message: 'Configure your preferences here.',
      placement: 'left',
      route: 'settings',
      beforeShow: () => router.push('/settings'),
    },
  ],
});

// Listen for route hints from the overlay
overlay.addEventListener('tour-route-change', (e) => {
  router.push(e.detail.route);
});
Tip: Add beforeShow to every step that needs a specific view — not just forward transitions. This ensures backward navigation (Back button) also lands on the correct page.

Multi-Page (MPA) Apps

For traditional multi-page apps where each URL is a full page load, Torchlit persists completed/dismissed state to localStorage. Register per-page tours on each page and use shouldAutoStart to resume.

// ── dashboard.html ──────────────────────────
import { createTourService } from 'torchlit';
import 'torchlit/overlay';

const tours = createTourService({ storageKey: 'my-app-tours' });

tours.register({
  id: 'dashboard-intro',
  name: 'Dashboard Intro',
  trigger: 'first-visit',
  steps: [
    { target: 'stats',   title: 'Stats',   message: 'Your key metrics.',   placement: 'bottom' },
    { target: 'actions', title: 'Actions',  message: 'Quick actions here.', placement: 'right' },
  ],
  onComplete: () => {
    // Guide user to the next page
    window.location.href = '/analytics';
  },
});

const overlay = document.querySelector('torchlit-overlay');
overlay.service = tours;

if (tours.shouldAutoStart('dashboard-intro')) {
  tours.start('dashboard-intro');
}
// ── analytics.html ──────────────────────────
// Same storageKey — state carries over!
const tours = createTourService({ storageKey: 'my-app-tours' });

tours.register({
  id: 'analytics-intro',
  name: 'Analytics Intro',
  trigger: 'first-visit',
  steps: [
    { target: 'chart', title: 'Charts', message: 'Your analytics.', placement: 'top' },
  ],
});

// ...
Key: Use the same storageKey across pages so completed/dismissed state is shared. Each page registers only its own tour.

Kiosk / Demo Mode

Combine loop: true with autoAdvance for hands-free, self-running tours. Perfect for expo booths, product demos, and digital signage.

tours.register({
  id: 'kiosk',
  name: 'Product Demo',
  trigger: 'manual',
  loop: true,  // restarts from step 0 after the last step
  steps: [
    {
      target: 'feature-1',
      title: 'Smart Search',
      message: 'Find anything in milliseconds.',
      placement: 'bottom',
      autoAdvance: 4000,  // advance after 4 seconds
    },
    {
      target: 'feature-2',
      title: 'Real-Time Sync',
      message: 'Changes appear instantly across devices.',
      placement: 'right',
      autoAdvance: 4000,
    },
    {
      target: 'feature-3',
      title: 'Team Collaboration',
      message: 'Work together seamlessly.',
      placement: 'left',
      autoAdvance: 4000,
    },
  ],
});

// Start the loop
tours.start('kiosk');
Note: Users can still exit with Escape or the Skip button. A progress bar renders automatically at the bottom of each tooltip.

Shadow DOM Targets

Torchlit's deepQuery utility traverses into shadow roots automatically. Just add data-tour-id to any element — even deeply nested inside web components.

<!-- Your custom element -->
<my-sidebar>
  #shadow-root
    <nav>
      <button data-tour-id="nav-home">Home</button>
      <my-dropdown>
        #shadow-root
          <div data-tour-id="nav-settings">Settings</div>
      </my-dropdown>
    </nav>
</my-sidebar>
// Torchlit finds both targets — no extra configuration
tours.register({
  id: 'nav-tour',
  name: 'Navigation',
  trigger: 'manual',
  steps: [
    { target: 'nav-home',     title: 'Home',     message: 'Go to the dashboard.', placement: 'right' },
    { target: 'nav-settings', title: 'Settings',  message: 'Found inside nested shadow DOM!', placement: 'right' },
  ],
});

You can also use a custom attribute instead of data-tour-id:

const tours = createTourService({
  targetAttribute: 'data-onboard',  // matches [data-onboard="..."]
});

Contextual Help

Register short manual tours per section, then wire a help button to start the right one based on the current view. This is the exact pattern used on this docs site.

// Register per-section help tours
tours.register([
  {
    id: 'dashboard-help',
    name: 'Dashboard Help',
    trigger: 'manual',
    steps: [
      { target: 'stats',   title: 'Stats',   message: 'Your daily metrics.',        placement: 'bottom' },
      { target: 'actions', title: 'Actions',  message: 'Quick actions are here.',    placement: 'right' },
    ],
  },
  {
    id: 'settings-help',
    name: 'Settings Help',
    trigger: 'manual',
    steps: [
      { target: 'profile', title: 'Profile', message: 'Update your info.',          placement: 'bottom' },
      { target: 'prefs',   title: 'Prefs',   message: 'Customize your experience.', placement: 'right' },
    ],
  },
]);

// Map views to their help tour
const helpMap = {
  dashboard: 'dashboard-help',
  settings:  'settings-help',
};

document.getElementById('btn-help').addEventListener('click', () => {
  const currentView = getCurrentView(); // your app logic
  const tourId = helpMap[currentView];
  if (tourId) tours.start(tourId);
});

Rich Content in Tooltips

Step messages accept Lit html tagged templates for rich HTML — bold, links, keyboard shortcuts, lists, and more.

import { html } from 'lit';

tours.register({
  id: 'rich-tour',
  name: 'Rich Content',
  trigger: 'manual',
  steps: [
    {
      target: 'editor',
      title: 'Keyboard Shortcuts',
      message: html`
        <p>Press <kbd>Ctrl+S</kbd> to save,
        <kbd>Ctrl+Z</kbd> to undo.</p>
        <p>See <a href="/docs/shortcuts"
          style="color: var(--primary)">all shortcuts</a>.</p>
      `,
      placement: 'bottom',
    },
    {
      target: 'toolbar',
      title: 'Formatting',
      message: html`
        <ul style="margin: 0.5rem 0; padding-left: 1.25rem;">
          <li><strong>Bold</strong> — Ctrl+B</li>
          <li><em>Italic</em> — Ctrl+I</li>
          <li><code>Code</code> — Ctrl+\`</li>
        </ul>
      `,
      placement: 'bottom',
    },
  ],
});

Custom Storage Adapter

By default Torchlit persists state to localStorage. You can swap in sessionStorage, an API backend, or any object with getItem / setItem.

sessionStorage

Show tours once per browser session instead of permanently:

const tours = createTourService({
  storage: sessionStorage,
});

API-Backed Persistence

Sync tour state to your server so it follows the user across devices:

const tours = createTourService({
  storage: {
    getItem(key) {
      // Return cached value synchronously;
      // hydrate from API on app startup
      return localStorage.getItem(key);
    },
    setItem(key, value) {
      localStorage.setItem(key, value);
      // Fire-and-forget API sync
      fetch('/api/tour-state', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({ key, value }),
      });
    },
  },
});
Note: The storage adapter must be synchronous (getItem returns a string or null). Use a local cache with async background sync as shown above.

API Reference

Complete reference for all exports, methods, types, and events.

createTourService<TStep = unknown>(config?)

Factory function that returns a new TourService instance. Import from torchlit or torchlit/service (the latter skips the overlay for service-only use and stays DOM-free).

import { createTourService } from 'torchlit';

const tours = createTourService({
  storageKey: 'my-app-tours',
  spotlightPadding: 12,
});

In TypeScript, pass a step type when you consume snapshot.step directly.

import { createTourService } from 'torchlit/service';
import type { TourStep } from 'torchlit';

const tours = createTourService<TourStep>();

Service changes in 0.3.0

  • TourService is now generic.
  • findTarget() moved out of the service API; use deepQuery(...).

TourConfig

PropertyTypeDefaultDescription
storageKeystring'torchlit-state'localStorage key for persisting completed/dismissed state
storageStorageAdapterlocalStorageCustom storage backend (must implement getItem / setItem)
targetAttributestring'data-tour-id'The DOM attribute used to find tour targets
spotlightPaddingnumber10Pixels of padding around the spotlight cutout

TourService Methods

Registration

MethodSignatureDescription
registerregister(tour: TourDefinition): voidRegister a single tour
registerregister(tours: TourDefinition[]): voidRegister multiple tours at once

Tour Control

MethodSignatureDescription
startstart(tourId: string): voidStart a tour by ID. No-op if the tour doesn't exist or has no steps.
nextStepnextStep(): voidAdvance to the next step. Completes the tour if on the last step.
prevStepprevStep(): voidGo back to the previous step. No-op if on step 0.
skipTourskipTour(): voidDismiss the active tour and persist it as "dismissed".

Queries

MethodSignatureDescription
isActiveisActive(): booleanWhether any tour is currently running
shouldAutoStartshouldAutoStart(tourId: string): booleanReturns true if the tour has trigger: 'first-visit' and hasn't been completed or dismissed
getTourgetTour(id: string): TourDefinition | undefinedLook up a registered tour by ID
getAvailableToursgetAvailableTours(): TourDefinition[]All registered tours
getSnapshotgetSnapshot(): TourSnapshot<TStep> | nullCurrent core service state for the active step (null if no tour is active)

Subscription

MethodSignatureDescription
subscribesubscribe(listener: TourListener): () => voidSubscribe to state changes. Returns an unsubscribe function. The overlay uses this internally.

Reset

MethodSignatureDescription
resetAllresetAll(): voidClear all persisted state, stop any active tour, and unregister all tours

Properties

PropertyTypeDescription
targetAttributereadonly stringThe data attribute used for target lookup. Set via TourConfig.
spotlightPaddingreadonly numberPadding in pixels around the spotlight. Set via TourConfig.

<torchlit-overlay>

The web component that renders the backdrop, spotlight, and tooltip. Import from torchlit or torchlit/overlay.

Properties

PropertyTypeDescription
serviceTourServiceRequired. The tour service instance that drives this overlay.

Events

EventDetailDescription
tour-route-change{ route: string }Fired when a step has a route property. The host application should handle navigation.

Keyboard Shortcuts

KeyAction
EscapeSkip / dismiss the tour
or EnterNext step
Previous step

CSS Parts

Style internal elements from outside the shadow DOM with ::part().

PartDescription
backdropThe semi-transparent overlay behind the spotlight
spotlightThe cutout highlight around the target element
tooltipThe floating tooltip card with title, message, and controls
center-cardThe centered card shown for steps with target: '_none_'
torchlit-overlay::part(tooltip) {
  box-shadow: 0 8px 30px rgba(0, 0, 0, 0.15);
}

torchlit-overlay::part(backdrop) {
  background: rgba(0, 0, 0, 0.6);
}

Types

TourPlacement

type TourPlacement = 'top' | 'bottom' | 'left' | 'right';

TourStep

PropertyTypeRequiredDescription
targetstringYesThe data-tour-id value to spotlight. Use '_none_' for a centered card.
titlestringYesBold title in the tooltip
messagestring | TemplateResultYesBody text in the tooltip. Accepts plain strings or Lit html`` templates for rich content.
placement'top' | 'bottom' | 'left' | 'right'YesPreferred tooltip position. Auto-flips when the viewport clips.
spotlightBorderRadiusstringNoOverride spotlight shape per step. '50%' for circle, '9999px' for pill, '0' for sharp.
autoAdvancenumberNoAuto-advance after N ms. A progress bar renders at the bottom of the tooltip. Manual interaction cancels.
routestringNoEmits tour-route-change so the app can navigate before this step
beforeShow() => void | Promise<void>NoAsync hook called before the step is displayed

TourDefinition

PropertyTypeRequiredDescription
idstringYesUnique tour identifier
namestringYesHuman-readable tour name
trigger'first-visit' | 'manual'Yesfirst-visit auto-starts once, manual requires start()
stepsTourStep[]YesOrdered array of steps
loopbooleanNoWhen true, advancing past the last step restarts at step 0 instead of completing. Combine with autoAdvance for kiosk / demo modes.
onEndScroll'restore' | 'top' | 'none'NoScroll behaviour on tour end. 'restore' (default) scrolls back to the pre-tour position; 'top' scrolls to top; 'none' leaves scroll as-is.
onComplete() => voidNoCalled when the user finishes the last step (not called when looping)
onSkip() => voidNoCalled when the user dismisses the tour

TourSnapshot

type TourSnapshot<TStep = unknown> = {
  tourId: string;
  tourName: string;
  step: TStep;
  stepIndex: number;
  totalSteps: number;
};
PropertyTypeDescription
tourIdstringID of the active tour
tourNamestringName of the active tour
stepTStepThe current step object
stepIndexnumberZero-based index of the current step
totalStepsnumberTotal number of steps in the tour

DOM-resolved target elements and bounding rects are overlay-internal resolved state and are not part of torchlit/service.

TourState

Persisted state managed by the service. Exposed as a type for custom storage adapters.

PropertyTypeDescription
completedstring[]Tour IDs that the user has fully completed
dismissedstring[]Tour IDs that the user has skipped / dismissed

StorageAdapter

interface StorageAdapter {
  getItem(key: string): string | null;
  setItem(key: string, value: string): void;
}

TourListener

type TourListener<TStep = unknown> = (snapshot: TourSnapshot<TStep> | null) => void;

deepQuery(selector, root?)

Finds the first element matching a CSS selector, traversing into shadow roots. Use it when you need public DOM or shadow DOM target lookup outside the overlay layer.

import { deepQuery } from 'torchlit';

// Find an element inside any shadow root
const el = deepQuery('[data-tour-id="my-target"]');

// Search within a specific subtree
const scoped = deepQuery('.btn', myComponent);

Styling

Customize the overlay with CSS custom properties, ::part() selectors, and theme tokens. Toggle dark mode with the sun icon above to see it in action.

Interactive Examples

Click Run Demo to try each mini-tour live, then View Code to see the configuration.

Navigation
Search
Profile
tours.register({
  id: 'basic',
  name: 'Basic Tour',
  trigger: 'manual',
  steps: [
    { target: 'nav',     title: 'Navigation', message: 'Move between pages.',   placement: 'bottom' },
    { target: 'search',  title: 'Search',     message: 'Find anything.',        placement: 'bottom' },
    { target: 'profile', title: 'Profile',    message: 'Manage your account.',  placement: 'bottom' },
  ],
});
Editor Area
Toolbar
import { html } from 'lit';

tours.register({
  id: 'rich',
  name: 'Rich Content',
  trigger: 'manual',
  steps: [
    {
      target: 'editor',
      title: 'Editor',
      message: html`Use <strong>bold</strong>, <em>italic</em>,
                     and <code>code</code> formatting.`,
      placement: 'bottom',
    },
    {
      target: 'toolbar',
      title: 'Toolbar',
      message: html`Shortcuts: <kbd>Ctrl+B</kbd> Bold
                     <kbd>Ctrl+I</kbd> Italic`,
      placement: 'bottom',
    },
  ],
});
Slide 1
Slide 2
Slide 3
tours.register({
  id: 'kiosk',
  name: 'Kiosk Demo',
  trigger: 'manual',
  loop: true,                           // restart from step 0
  steps: [
    { target: 'slide-1', title: 'Slide 1', message: 'Auto-advances.', placement: 'bottom', autoAdvance: 2500 },
    { target: 'slide-2', title: 'Slide 2', message: 'No clicks needed.', placement: 'bottom', autoAdvance: 2500 },
    { target: 'slide-3', title: 'Slide 3', message: 'Loops back.',      placement: 'bottom', autoAdvance: 2500 },
  ],
});
T
Pill Element
Square
tours.register({
  id: 'shapes',
  name: 'Spotlight Shapes',
  trigger: 'manual',
  steps: [
    { target: 'avatar', title: 'Circle', message: 'Circular cutout.', placement: 'bottom', spotlightBorderRadius: '50%' },
    { target: 'pill',   title: 'Pill',   message: 'Pill shape.',      placement: 'bottom', spotlightBorderRadius: '9999px' },
    { target: 'square', title: 'Square', message: 'Sharp corners.',   placement: 'bottom', spotlightBorderRadius: '0' },
  ],
});

CSS Custom Properties

Set these on :root or on torchlit-overlay directly. Each --tour-* variable falls back to a generic design token, then to a hardcoded default.

PropertyFallbackDefaultUsed for
--tour-primary--primary#F26122Accent color, progress dots, pulse ring
--tour-primary-foreground--primary-foreground#fffText on primary-colored elements
--tour-card--card#fffTooltip and center-card background
--tour-foreground--foreground#1a1a1aTitle and body text
--tour-muted-foreground--muted-foreground#737373Step counter, secondary text
--tour-muted--muted#e5e5e5Inactive progress dots, button hover
--tour-border--border#e5e5e5Tooltip and button borders
--tour-background--background#fffSecondary button background
--tour-spotlight-radius--radius-lg0.75remSpotlight border-radius
--tour-tooltip-radius--radius-lg0.75remTooltip border-radius
--tour-card-radius--radius-xl1remCenter-card border-radius
--tour-btn-radius--radius-md0.5remButton border-radius

Theme Playground

Tweak CSS custom property values and see the result in real time. Copy the generated CSS when you're happy with the look.

Live Preview
STEP 2 OF 4
Navigation
Use the sidebar to move between pages.

Example: Custom Theme

:root {
  /* Brand colors */
  --tour-primary: #6366f1;            /* Indigo */
  --tour-primary-foreground: #fff;

  /* Surface colors */
  --tour-card: #fafafa;
  --tour-foreground: #18181b;
  --tour-border: #e4e4e7;

  /* Rounder corners */
  --tour-spotlight-radius: 1rem;
  --tour-tooltip-radius: 1rem;
  --tour-btn-radius: 9999px;          /* Pill buttons */
}

Dark Mode

If your app uses a .dark class or prefers-color-scheme media query, just override the fallback tokens. Torchlit picks them up automatically.

:root.dark {
  --primary: #FF8C59;
  --primary-foreground: #1a1a1a;
  --card: #252525;
  --foreground: #e8e8e8;
  --border: #3a3a3a;
  --muted: #2f2f2f;
  --muted-foreground: #9ca3af;
}

/* Or use --tour-* for overlay-only overrides */
:root.dark {
  --tour-primary: #FF8C59;
  --tour-card: #252525;
}

CSS ::part() Selectors

For deeper customization beyond colors, use ::part() to style internal elements. See the API Reference for the full list of parts.

/* Larger tooltip shadow */
torchlit-overlay::part(tooltip) {
  box-shadow: 0 25px 50px rgba(0, 0, 0, 0.25);
}

/* Custom backdrop opacity */
torchlit-overlay::part(backdrop) {
  background: rgba(0, 0, 0, 0.7);
}

/* Glow effect on spotlight */
torchlit-overlay::part(spotlight) {
  box-shadow: 0 0 0 4px rgba(242, 97, 34, 0.3);
}