Torchlit
Shadow DOM-aware guided tours for any web framework. Lightweight, framework-agnostic, and built on Lit web components.
deepQuery to traverse any depth.role="dialog", focus trapping, keyboard navigation, and aria-live announcements for screen readers.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.
Add Torchlit and its Lit peer dependency.
npm install torchlit lit
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>().
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>
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'),
});
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
TourServiceis now generic.findTarget()moved out of the service API; usedeepQuery(...).
TourConfig
| Property | Type | Default | Description |
|---|---|---|---|
storageKey | string | 'torchlit-state' | localStorage key for persisting completed/dismissed state |
storage | StorageAdapter | localStorage | Custom storage backend (must implement getItem / setItem) |
targetAttribute | string | 'data-tour-id' | The DOM attribute used to find tour targets |
spotlightPadding | number | 10 | Pixels of padding around the spotlight cutout |
TourService Methods
Registration
| Method | Signature | Description |
|---|---|---|
register | register(tour: TourDefinition): void | Register a single tour |
register | register(tours: TourDefinition[]): void | Register multiple tours at once |
Tour Control
| Method | Signature | Description |
|---|---|---|
start | start(tourId: string): void | Start a tour by ID. No-op if the tour doesn't exist or has no steps. |
nextStep | nextStep(): void | Advance to the next step. Completes the tour if on the last step. |
prevStep | prevStep(): void | Go back to the previous step. No-op if on step 0. |
skipTour | skipTour(): void | Dismiss the active tour and persist it as "dismissed". |
Queries
| Method | Signature | Description |
|---|---|---|
isActive | isActive(): boolean | Whether any tour is currently running |
shouldAutoStart | shouldAutoStart(tourId: string): boolean | Returns true if the tour has trigger: 'first-visit' and hasn't been completed or dismissed |
getTour | getTour(id: string): TourDefinition | undefined | Look up a registered tour by ID |
getAvailableTours | getAvailableTours(): TourDefinition[] | All registered tours |
getSnapshot | getSnapshot(): TourSnapshot<TStep> | null | Current core service state for the active step (null if no tour is active) |
Subscription
| Method | Signature | Description |
|---|---|---|
subscribe | subscribe(listener: TourListener): () => void | Subscribe to state changes. Returns an unsubscribe function. The overlay uses this internally. |
Reset
| Method | Signature | Description |
|---|---|---|
resetAll | resetAll(): void | Clear all persisted state, stop any active tour, and unregister all tours |
Properties
| Property | Type | Description |
|---|---|---|
targetAttribute | readonly string | The data attribute used for target lookup. Set via TourConfig. |
spotlightPadding | readonly number | Padding 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
| Property | Type | Description |
|---|---|---|
service | TourService | Required. The tour service instance that drives this overlay. |
Events
| Event | Detail | Description |
|---|---|---|
tour-route-change | { route: string } | Fired when a step has a route property. The host application should handle navigation. |
Keyboard Shortcuts
| Key | Action |
|---|---|
| Escape | Skip / dismiss the tour |
| → or Enter | Next step |
| ← | Previous step |
CSS Parts
Style internal elements from outside the shadow DOM with ::part().
| Part | Description |
|---|---|
backdrop | The semi-transparent overlay behind the spotlight |
spotlight | The cutout highlight around the target element |
tooltip | The floating tooltip card with title, message, and controls |
center-card | The 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
| Property | Type | Required | Description |
|---|---|---|---|
target | string | Yes | The data-tour-id value to spotlight. Use '_none_' for a centered card. |
title | string | Yes | Bold title in the tooltip |
message | string | TemplateResult | Yes | Body text in the tooltip. Accepts plain strings or Lit html`` templates for rich content. |
placement | 'top' | 'bottom' | 'left' | 'right' | Yes | Preferred tooltip position. Auto-flips when the viewport clips. |
spotlightBorderRadius | string | No | Override spotlight shape per step. '50%' for circle, '9999px' for pill, '0' for sharp. |
autoAdvance | number | No | Auto-advance after N ms. A progress bar renders at the bottom of the tooltip. Manual interaction cancels. |
route | string | No | Emits tour-route-change so the app can navigate before this step |
beforeShow | () => void | Promise<void> | No | Async hook called before the step is displayed |
TourDefinition
| Property | Type | Required | Description |
|---|---|---|---|
id | string | Yes | Unique tour identifier |
name | string | Yes | Human-readable tour name |
trigger | 'first-visit' | 'manual' | Yes | first-visit auto-starts once, manual requires start() |
steps | TourStep[] | Yes | Ordered array of steps |
loop | boolean | No | When true, advancing past the last step restarts at step 0 instead of completing. Combine with autoAdvance for kiosk / demo modes. |
onEndScroll | 'restore' | 'top' | 'none' | No | Scroll behaviour on tour end. 'restore' (default) scrolls back to the pre-tour position; 'top' scrolls to top; 'none' leaves scroll as-is. |
onComplete | () => void | No | Called when the user finishes the last step (not called when looping) |
onSkip | () => void | No | Called when the user dismisses the tour |
TourSnapshot
type TourSnapshot<TStep = unknown> = {
tourId: string;
tourName: string;
step: TStep;
stepIndex: number;
totalSteps: number;
};
| Property | Type | Description |
|---|---|---|
tourId | string | ID of the active tour |
tourName | string | Name of the active tour |
step | TStep | The current step object |
stepIndex | number | Zero-based index of the current step |
totalSteps | number | Total 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.
| Property | Type | Description |
|---|---|---|
completed | string[] | Tour IDs that the user has fully completed |
dismissed | string[] | 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.
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' },
],
});
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',
},
],
});
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 },
],
});
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.
| Property | Fallback | Default | Used for |
|---|---|---|---|
--tour-primary | --primary | #F26122 | Accent color, progress dots, pulse ring |
--tour-primary-foreground | --primary-foreground | #fff | Text on primary-colored elements |
--tour-card | --card | #fff | Tooltip and center-card background |
--tour-foreground | --foreground | #1a1a1a | Title and body text |
--tour-muted-foreground | --muted-foreground | #737373 | Step counter, secondary text |
--tour-muted | --muted | #e5e5e5 | Inactive progress dots, button hover |
--tour-border | --border | #e5e5e5 | Tooltip and button borders |
--tour-background | --background | #fff | Secondary button background |
--tour-spotlight-radius | --radius-lg | 0.75rem | Spotlight border-radius |
--tour-tooltip-radius | --radius-lg | 0.75rem | Tooltip border-radius |
--tour-card-radius | --radius-xl | 1rem | Center-card border-radius |
--tour-btn-radius | --radius-md | 0.5rem | Button 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.
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);
}