This is the full developer documentation for Docent # Show people around your product. > Docent spotlights one element, explains it in a sentence or two, and gets out of the way. Tours are plain JSON, so they are easy to write, review and change. How it works ## A tour is a small JSON document. You describe the steps, point each one at an element, and start it. Docent draws the spotlight, places the popover, follows the element as the page scrolls and resizes, and remembers where each user got to. 01 ### Describe the steps Each step has a title, a sentence or two, and the element it points at. ```ts const tour = defineTour({ id: 'welcome', steps: [ { id: 'hi', title: 'Welcome' }, { id: 'new', target: { name: 'new' }, title: 'Create a project' }, ], }) ``` 02 ### Name the elements A `data-docent` name survives redesigns and refactors, unlike a CSS class. ```html ``` 03 ### Start it Start one tour yourself, or let the manager decide who sees which tour, and when. ```ts createTour(tour).start() // or, for many tours with rules: createDocent({ tours: [welcome, billing] }) ``` Live ## Change a value. The tour follows. Below, a tour is being edited on its own and the real library redraws the step after each change: the arrow, the spotlight, the theme, even which element it points at. Watch it as the file, or as the devtools panel you would use in your own app. Tour fileDevtools An animation of a tour being edited, either as its JSON file or in the Docent devtools panel, with a preview that updates after each change. welcome.tour.jsonPreviewChecked · no problems**Playing on its own 1. { 2. "id": "welcome", 3. "options": { 4. "arrow": "", 5. "spotlight": { "shape": "", "ring": "" }, 6. "progress": "", 7. "theme": { "preset": "", "width": 260 } 8. }, 9. "steps": \[ 10. { "id": "hello" … }, 11. { 12. "id": "invite", 13. "target": { "name": "" }, 14. "title": "", 15. "body": "", 16. "placement": "" 17. }, 18. { "id": "done" … } 19. ] 20. } localhost:5173 · NorthwindDevtools · Edit tab**Playing on its own Make it yours ## Quiet by default. Expressive when you want it. The default is a small caret, a soft spotlight and a dimmed page. Pick anything below to see it on the button. Every choice is one field in the tour's JSON, for the whole tour or a single step. Or start from a [ready-made theme](/customize/themes/), one command away. Arrow \[ ]caret\[ ]none\[ ]line\[ ]dashed\[ ]dotted\[x]curve\[ ]curve-dashed\[ ]squiggle\[ ]loop\[ ]elbow\[ ]sketch\[ ]pin Spotlight shape \[ ]rounded\[ ]rect\[x]pill\[ ]circle Ring \[x]hairline\[ ]none\[ ]glow\[ ]pulse\[ ]dashed\[ ]solid Overlay \[x]dim\[ ]blur\[ ]vignette\[ ]none Share report Preview Choosing an option plays it. Press Escape or click the page to close. ``` ``` Details ## Made carefully where it counts. * Small About **15 kB** compressed for the engine and the web renderer together. Drawn arrows load only when a tour uses one. * Accessible Dialog semantics, focus kept inside the popover and returned after, full keyboard control, reduced motion, and 44 px touch targets. * Isolated The popover renders in a shadow root. Your CSS cannot break it, and its CSS cannot leak into your page. * Mobile-ready On narrow screens the popover docks near the bottom as a card and scrolls the target clear of it. * Data, not code Tours are JSON. Store them in your repo, load them from an API, or edit them in a builder. * Rules built in Triggers, conditions on user traits, and per-user frequency decide who sees which tour. * Any framework React, Vue and Svelte bindings, each about 1 kB. Or none at all. * Devtools See why a tour is or is not showing, edit it live, audit targets and contrast. * Good with AI A published [JSON Schema](/schema/tour-v1.json), [plain-text docs](/llms.txt) and a checker, so an assistant can write tours and prove they are valid. Frameworks ## Fits the app you already have. One package per framework, each a thin layer over the same engine. Use the built-in popover, or render your own component and keep the spotlight, positioning and keyboard handling. [React Hooks and a component](/frameworks/react/)[Vue Composables and a teleport](/frameworks/vue/)[Svelte Stores and an action](/frameworks/svelte/)[No framework Plain JavaScript](/getting-started/)[Examples Four apps, running](/examples/) * React ```tsx import { DocentDevtools } from '@docentjs/devtools/react' import { useDocent } from '@docentjs/react' import { billing, welcome } from './tours' export function App({ user }) { const docent = useDocent({ tours: [welcome, billing] }) useEffect(() => docent.identify(user.id, { plan: user.plan }), [user]) return } ``` * Vue ```vue ``` * Svelte ```svelte ``` * Vanilla ```ts import { createDocent } from '@docentjs/dom' import { billing, welcome } from './tours' const docent = createDocent({ tours: [welcome, billing] }) docent.identify(user.id, { plan: user.plan }) ``` Start here ## Your first tour takes about five minutes. [Read the guide](/getting-started/)[View on GitHub](https://github.com/FgrReloaded/docentjs) # Getting started > Install Docent, describe a tour, and show it to someone. About five minutes from nothing to a working tour. A **tour** walks someone through your interface one element at a time. Each **step** dims the page, spotlights one element, and explains it in a small popover with Back and Next buttons. Docent handles the positioning, scrolling, keyboard and focus. You write the words and decide which element each step points at. 1. **Install the package for your stack.** * Vanilla ```sh pnpm add @docentjs/dom ``` * React ```sh pnpm add @docentjs/react ``` * Vue ```sh pnpm add @docentjs/vue ``` * Svelte ```sh pnpm add @docentjs/svelte ``` One package is enough. The framework packages include the web renderer and the engine, and re-export everything you need, so in a React app every import comes from `@docentjs/react`. 2. **Describe the tour.** A tour is a plain object: an `id` and a list of `steps`. `defineTour` adds type checking and stamps the schema version. tours.ts ```ts import { defineTour } from '@docentjs/dom' export const welcome = defineTour({ id: 'welcome', options: { showProgress: true }, steps: [ { id: 'intro', title: 'Welcome', body: 'This takes about a minute.' }, { id: 'sidebar', target: { name: 'sidebar' }, title: 'Navigation', placement: 'right' }, { id: 'new', target: '#new-project', title: 'Create a project', advance: { on: 'click' } }, { id: 'done', title: 'All set' }, ], }) ``` A step without a `target` shows as a centred card, which suits a welcome and a goodbye. The last-but-one step uses `advance: { on: 'click' }`, so it moves on when the person clicks the button instead of pressing Next. 3. **Mark the elements the steps point at.** ```html ``` `target: { name: 'sidebar' }` finds `data-docent="sidebar"`. A plain string such as `'#new-project'` is a CSS selector. Names are the better habit: they survive redesigns, and they say plainly that an element is part of a tour. 4. **Start it.** ```ts import { createTour } from '@docentjs/dom' import { welcome } from './tours' createTour(welcome).start() ``` `createTour` connects the web renderer, saves progress to `localStorage`, and follows client-side navigation. Call `resume()` instead of `start()` to continue from where the person left off. Here is the same tour running on this page: LiveThe tour from the steps abovePlay SidebarNew project Showing tours automatically `createTour(...).start()` shows a tour when you call it. To show tours on their own, for example once to new users on their first visit, hand them to the [tour manager](/guides/manager/) instead: ```ts import { createDocent } from '@docentjs/dom' const docent = createDocent({ tours: [welcome, invoices] }) docent.identify(user.id, { plan: user.plan }) ``` Each tour’s `trigger`, `conditions` and `frequency` then decide who sees it and when. ## Where to go next [Section titled “Where to go next”](#where-to-go-next) [Concepts](/concepts/)How tours, the engine and the renderer fit together. [Targets](/guides/targets/)Point steps at elements reliably, including ones that load late. [Tour manager](/guides/manager/)Many tours, shown to the right people at the right time. [Theming](/customize/theming/)Make the popover match your product. # Concepts > The few ideas behind Docent. Read this once and the rest of the docs will make sense. ## A tour is data [Section titled “A tour is data”](#a-tour-is-data) Everything about a tour that can be written down lives in one JSON document: its steps, what each step points at, how it moves on, when the tour starts, who should see it, and how it looks. ```ts { schemaVersion: 1, id: 'invoices', trigger: { type: 'route', pattern: '/invoices' }, conditions: [{ type: 'trait', key: 'plan', op: 'eq', value: 'trial' }], options: { showProgress: true, theme: { accent: '#0d9488' } }, steps: [{ id: 'new', target: { name: 'new-invoice' }, title: 'Create an invoice' }], } ``` Because it is data, a tour can sit in your repo, come from an API, be edited in the [devtools](/guides/devtools/), and later come from a visual builder, all without code changes. `schemaVersion` lets the format grow without breaking old tours. Only two things are code instead of data: **hooks**, the functions that run around a step, and **custom rendering**. Both attach in your app, keyed by step id, so the JSON stays portable. ## Three layers [Section titled “Three layers”](#three-layers) | Layer | Package | Job | | -------- | ------------------------------------ | ------------------------------------------------------------------------------------------------ | | Engine | `@docentjs/core` | Tour state, conditions, routes, progress and events. No DOM, so it runs anywhere. | | Renderer | `@docentjs/dom` | Finds elements, draws the overlay and spotlight, places the popover, handles keyboard and focus. | | Adapters | `@docentjs/react`, `/vue`, `/svelte` | Connect a tour’s lifecycle and state to your framework, and let you render your own popover. | The engine never touches the page, which keeps it small and testable. The same engine can drive native renderers for iOS and Android later, from the same tour JSON. ## Two ways to run tours [Section titled “Two ways to run tours”](#two-ways-to-run-tours) **One tour, on demand.** `createTour(tour)` returns a controller. You decide when it starts, typically from a “Take the tour” button. ```ts const tour = createTour(welcome) tour.start() // or tour.resume() to continue saved progress tour.next(); tour.back(); tour.skip(); tour.goTo('step-id') tour.notify('saved') // moves on a step waiting for { on: 'event', name: 'saved' } tour.subscribe((state) => console.log(state.status, state.index)) tour.destroy() ``` **Many tours, by rule.** `createDocent({ tours })` returns the [tour manager](/guides/manager/). It watches each tour’s trigger, checks its conditions against the current user, respects how often it may show, and runs one tour at a time. ## Seams [Section titled “Seams”](#seams) Four small interfaces connect Docent to the rest of your system. Each has a sensible default, so you only replace the ones you need. | Seam | What it provides | Default on the web | | ---------------- | --------------------------------- | -------------------------------------- | | `TourSource` | the tours | the array you pass in | | `Identity` | who the user is, and their traits | anonymous | | `StorageAdapter` | where progress is saved | `localStorage`, falling back to memory | | `EventSink` | where lifecycle events go | nowhere | Point them at your own backend, or a hosted service, without touching a single tour. ## How it renders [Section titled “How it renders”](#how-it-renders) The overlay, spotlight and built-in popover render inside a [shadow root](https://developer.mozilla.org/en-US/docs/Web/API/Web_components/Using_shadow_DOM). Your page’s CSS cannot reach in and break them, and theirs cannot leak out and break your page. Theme tokens are CSS custom properties, which do cross that boundary, on purpose. That is how [theming](/customize/theming/) works. Note Content you supply yourself, through [slots](/customize/slots-and-templates/) or a [custom popover](/customize/headless/), stays in your page’s normal DOM. Your styles and your framework’s event handling keep working on it. # API > Every export, package by package. For the fields of a tour itself, see the tour schema. Most apps use one package: `@docentjs/dom` without a framework, or the adapter for their framework. The adapters re-export what you need from the packages below them. ## `@docentjs/dom` [Section titled “@docentjs/dom”](#docentjsdom) The web renderer, plus browser-ready versions of the engine’s entry points. | Export | What it is | | ------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `createTour(tour, options?)` | a controller for one tour, with the DOM renderer, `localStorage` progress and route following connected; see [the controller](#tourcontroller) | | `createDocent(options?)` | the [tour manager](/guides/manager/), ready for the browser; see [the manager](#docent) | | `defineTour(tour)` | returns the tour with `schemaVersion` set, and checks its type | | `createLocalStorage()` | a storage adapter over `localStorage` that falls back to memory | | `DomRenderer` | the renderer itself, for use with `TourController` directly | | `computePosition`, `resolveTarget`, `waitForTarget`, `anchorPoint` | the positioning, target and beacon-placement helpers, for building your own renderer pieces | | `createDomEnvironment(document?, renderer?)` | what `createDocent` gives the manager in the browser: routes, element watching and [beacons](/guides/beacons/) | `createTour` options: everything in `ControllerOptions` below, plus `renderer` (renderer options) and `followRoutes`. `createDocent` options: `tours` (an array or a `TourSource`), `identity`, `storage`, `sink`, `hooks` keyed by tour id, `custom` predicates, `minViewportWidth` (no tours below this width, in px), `renderer`, `document`, and `connect` (`false` to start watching later with `connect()`). ### Renderer options [Section titled “Renderer options”](#renderer-options) | Option | Purpose | | ------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `theme` | [theme tokens](/customize/theming/) for every tour | | `arrow`, `spotlight`, `overlay` | default [looks](/customize/arrows-and-spotlight/) | | `beacon` | default [beacon](/guides/beacons/) look: `style`, `position`, `offset`, `size` | | `progress`, `eyebrow` | default step counter and the line above the title; see [themes](/customize/themes/) | | `slots`, `templates` | [slots and templates](/customize/slots-and-templates/) | | `template` | a registered name, a built-in look, or a [theme](/customize/themes/) object | | `headless` | [your own popover](/customize/headless/) | | `css` | extra CSS inside the shadow root | | `labels` | button and progress wording | | `gap` | space between the popover and the target, in px. Drawn arrows use it as a minimum and take more room if their style needs it | | `sheetBreakpoint` | the width below which the screen counts as small: the popover becomes a full-width card beside the target, or docked when it does not fit (default `480`; `0` never) | | `mobile` | small-screen settings when a tour sets none, e.g. `{ layout: 'dock', card: 'classic' }` | | `avoidOcclusion` | move the target out from under sticky headers (default `true`) | | `focus` | move focus into the popover when a tour opens and back when it ends (default `true`); tours opened by hovering a beacon never take focus | | `document` | render into another document, such as an iframe | ### `@docentjs/dom/validate` [Section titled “@docentjs/dom/validate”](#docentjsdomvalidate) The schema checks, for development and CI: `validateTour`, `validateTheme`, `isValidTour`, `formatIssues` and `tourJsonSchema`. Re-exported from `@docentjs/core/validate`; see [checking a tour](/reference/schema/#checking-a-tour). ### `@docentjs/dom/themes` [Section titled “@docentjs/dom/themes”](#docentjsdomthemes) The presets `light`, `dark`, `minimal` and `contrast`, and `presets`, all four by name. ## `@docentjs/core` [Section titled “@docentjs/core”](#docentjscore) The engine. It has no DOM code and runs anywhere. ### `TourController` [Section titled “TourController”](#tourcontroller) Drives one tour against a renderer. `createTour` returns a subclass with everything connected. | Method | What it does | | ------------------------------------------ | -------------------------------------------------------------- | | `start(at?)` | start from the beginning, or from a step id or index | | `resume()` | continue saved progress, or start | | `next()`, `back()`, `skip()`, `goTo(step)` | move through the tour | | `abort(reason)` | end the tour, recorded as aborted | | `notify(name)` | move on a step waiting for `{ on: 'event', name }` | | `routeChanged()` | re-check the current route after a navigation it could not see | | `updateTour(tour)` | replace the definition while it runs, keeping the current step | | `getState()`, `subscribe(fn)` | the engine state: `{ status, index, history, reason? }` | | `destroy()` | stop and remove everything | `ControllerOptions`: `tour`, `renderer`, `identity`, `storage`, `sink`, `hooks`, `custom`, `tourState`, `defaultWaitMs`, `now`. ### `Docent` [Section titled “Docent”](#docent) The tour manager. Build it with `createDocent` from the dom package, which supplies the browser environment. | Member | What it does | | ------------------------------------------------ | ---------------------------------------------------------- | | `ready` | a promise that resolves once tours and progress are loaded | | `identify(id, traits)` | set the current user | | `track(name)` | report an event | | `start(id, { at? })`, `stop()` | start a tour by hand, or end the running one | | `reset(id?)`, `refresh()` | forget progress; re-check triggers | | `isEligible(id)`, `tourState(id)` | whether a tour would show now; its progress for this user | | `updateTour(tour)` | replace a tour’s definition, even while it runs | | `getState()`, `subscribe(fn)` | `{ active, tours }` | | `getTours()`, `onEvent(fn)`, `getConditionEnv()` | for tooling such as the devtools | | `activeController` | the running tour’s controller | | `connect()`, `disconnect()`, `destroy()` | start or stop watching; remove everything | ### Helpers and types [Section titled “Helpers and types”](#helpers-and-types) * **Helpers:** `beaconTarget`, `reduce`, `evaluateCondition`, `evaluateAll`, `matchRoute`, `shouldShow`, `ProgressStore`, `createMemoryStorage`, `createEvent`, `combineSinks`, `NOOP_SINK`. * **Types:** everything in the [tour schema](/reference/schema/), plus `Renderer`, `RenderContext`, `EngineState`, `TourHooks`, `StepHooks`, `DocentEvent`, `DocentEnvironment` (with `BeaconRequest` and `BeaconHandle` for platforms that draw beacons), and the four seams: `TourSource`, `Identity`, `StorageAdapter`, `EventSink`. ## `@docentjs/react` [Section titled “@docentjs/react”](#docentjsreact) | Export | What it is | | ------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- | | `useTour(tour, options?)` | `{ state, active, start, resume, next, back, skip, goTo, notify, controller, portal }`; `options.popover` renders your own popover | | `useDocent(options?)` | the manager for the component’s lifetime: `{ state, docent, identify, track, start, stop, reset, refresh, portal }` | | `` | the component form; children receive the controls | | `` | shares `renderer`, `identity`, `storage` and `sink` with every tour below it | ## `@docentjs/vue` [Section titled “@docentjs/vue”](#docentjsvue) | Export | What it is | | -------------------------------------- | ----------------------------------------------------------------------------------------- | | `useTour(tour, options?)` | refs `state`, `active`, `popoverContext`, `popoverTarget`, the controls, and `destroy` | | `useDocent(options?)` | a reactive `state`, the manager methods, `popoverContext`, `popoverTarget`, and `destroy` | | `` | teleports its slot into the positioned box when `popover: true` | | `provideDocentDefaults(defaults)` | shares defaults with descendants | ## `@docentjs/svelte` [Section titled “@docentjs/svelte”](#docentjssvelte) | Export | What it is | | ------------------------- | ------------------------------------------------------------------------------------------ | | `useTour(tour, options?)` | stores `state` and `active`, the controls, and `destroy`; `options.popover` is a component | | `useDocent(options?)` | a `state` store, the manager methods, and `destroy` | | `use:tour={handle}` | starts on mount, cleans up on removal | ## `@docentjs/devtools` [Section titled “@docentjs/devtools”](#docentjsdevtools) | Export | What it is | | ---------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `DocentDevtools` from `/react`, `/vue`, `/svelte` | a component that mounts the panel in development and renders nothing in production | | `mount(docent, { open?, shortcut?, persist?, document? })` | mounts the panel directly and returns a function that removes it | ## `@docentjs/cli` [Section titled “@docentjs/cli”](#docentjscli) ```sh npx @docentjs/cli validate "tours/*.json" ``` | Command | What it does | | ------------------------------ | -------------------------------------------------------------------------------- | | `docent validate ` | check tour files against the schema; exits 1 when anything is wrong | | `docent schema [file]` | print the tour JSON Schema, or write it to a file | | `docent theme list` | list the ready-made [themes](/customize/themes/) | | `docent theme add ` | check a theme and write it to `docent-theme.json`; refuses one that is not valid | Options for `validate`: `--json`, `--quiet`, `--allow-unknown`. For `theme add`: `--out ` to write elsewhere, `--force` to replace an existing file. ## Addresses [Section titled “Addresses”](#addresses) | Address | What it is | | ------------------------------------------------------------------------------------------------------------ | ---------------------------------- | | [/schema/tour-v1.json](https://docentjs.dev/schema/tour-v1.json) | the tour format as JSON Schema | | [/llms.txt](https://docentjs.dev/llms.txt) | a summary for AI tools, with links | | [/llms-small.txt](https://docentjs.dev/llms-small.txt), [/llms-full.txt](https://docentjs.dev/llms-full.txt) | the documentation as plain text | See [Using Docent with AI](/reference/for-ai/). # Using Docent with AI > Plain-text documentation, a machine-readable schema and a checker, so an assistant can write and change tours correctly. Tours are data, which makes them a good fit for an assistant: it can write one, change one, and check its work without running a browser. These four addresses are what it needs. | Address | What it is | | ---------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | | [docentjs.dev/llms.txt](https://docentjs.dev/llms.txt) | A short summary of Docent, the rules worth following, and links to everything else. Start here. | | [docentjs.dev/llms-small.txt](https://docentjs.dev/llms-small.txt) | The documentation as plain text, trimmed of asides and extras. Fits a smaller context window. | | [docentjs.dev/llms-full.txt](https://docentjs.dev/llms-full.txt) | Every page as plain text, complete. | | [docentjs.dev/schema/tour-v1.json](https://docentjs.dev/schema/tour-v1.json) | The tour format as JSON Schema: every field, type and allowed value. | They are generated from these pages on every build, so they never fall behind. ## Pointing a tool at them [Section titled “Pointing a tool at them”](#pointing-a-tool-at-them) Most assistants take a URL directly: paste the `llms.txt` address and ask it to read the linked documentation. In editors, give it whichever fits the window, `llms-small.txt` or `llms-full.txt`. For tour files in your repository, the schema is more useful than any prose. Add it to the file and most editors will complete fields and mark mistakes as you type, with or without an assistant: tours/welcome.tour.json ```json { "$schema": "https://docentjs.dev/schema/tour-v1.json", "id": "welcome", "steps": [{ "id": "intro", "title": "Welcome" }] } ``` ## Checking the result [Section titled “Checking the result”](#checking-the-result) Never take a generated tour on trust. The checker reports unknown values, misspelled fields, wrong types and duplicate step ids, and names the value that was probably meant: ```sh npx @docentjs/cli validate "tours/*.json" ``` It exits 1 when anything is wrong, so it works as a step in CI or as the last step of an assistant’s own loop. `--json` gives output another program can read. The same checks run automatically in development, and [`validateTour`](/reference/schema/#checking-a-tour) is available in code. What a check cannot tell you A valid tour can still point at the wrong button or explain the wrong thing. The [devtools](/guides/devtools/) Audit tab measures the real page: targets that are missing, invisible, covered or too small, and text contrast. Play the tour once before shipping it. ## Advice worth passing on [Section titled “Advice worth passing on”](#advice-worth-passing-on) These are the habits that keep generated tours working: * Target elements by name, with `data-docent="save"` in the markup and `target: { name: 'save' }` in the tour. Generated CSS selectors break at the next redesign. * Keep visual choices in the tour JSON, through `theme`, `arrow`, `progress`, `eyebrow`, `spotlight`, `overlay` and `appearance`, rather than in stylesheets. They travel with the tour. * For a whole look, use a [theme](/customize/themes/): one JSON file. `npx @docentjs/cli theme add ` writes one after checking it; then `renderer: { template: theme }`. Slots take functions, so a look that uses them can no longer be published or edited as data. * One idea per step. A step that needs three sentences is usually two steps. * Use `onMissing: 'wait'` for anything that renders late, such as a menu or a list from an API. * For a hint someone can open when they want it, rather than a tour that interrupts, use a [beacon](/guides/beacons/): `trigger: { type: 'beacon' }` on a one-step tour whose step has a target. `open: 'hover'` with `options.beacon.style: 'none'` makes a plain tooltip. # Tour schema > Every field of the tour JSON document. A tour is one JSON document. This page lists every field. All fields are optional unless marked required, and every type is exported from `@docentjs/core` and re-exported by the other packages. The schema is published at [docentjs.dev/schema/tour-v1.json](https://docentjs.dev/schema/tour-v1.json). Tours written as `.json` files can point at it, which gives most editors completion and checking as you type: ```json { "$schema": "https://docentjs.dev/schema/tour-v1.json", "id": "welcome", "steps": [{ "id": "intro", "title": "Welcome" }] } ``` The guides explain each area with examples: [steps](/guides/steps/), [targets](/guides/targets/), [triggers and conditions](/guides/triggers-and-conditions/), [beacons](/guides/beacons/), [theming](/customize/theming/), and [arrows, spotlight and overlay](/customize/arrows-and-spotlight/). ## Tour [Section titled “Tour”](#tour) | Field | Type | Notes | | --------------------- | ------------------------- | ---------------------------------------------------------- | | `schemaVersion` | `1` | required; `defineTour` sets it | | `id` | `string` | required; used for persistence, targeting, analytics | | `version` | `number` | bump to re-show the tour to users who saw an older version | | `name`, `description` | `string` | for the builder and dashboards | | `steps` | `Step[]` | required | | `trigger` | `Trigger` | what starts the tour | | `conditions` | `Condition[]` | all must hold for the tour to be eligible | | `options` | `TourOptions` | see below | | `meta` | `Record` | free-form extension bag | ## TourOptions [Section titled “TourOptions”](#touroptions) | Field | Type | Default | | --------------------- | ------------------------------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `persist` | `boolean` | – | | `frequency` | `'once' \| 'until-completed' \| 'always'` | `'once'` | | `showProgress` | `boolean` | `true` | | `progress` | `ProgressStyle` | `'meter'`; see [Themes](/customize/themes/) | | `eyebrow` | `string` | – ; a small line above every title. `{tour}` becomes the tour’s name | | `allowClose` | `boolean` | `true` (Escape and the close button) | | `closeOnOverlayClick` | `boolean` | `false` | | `closeOnOutsideClick` | `boolean` | `true` for beacon tours, else `false`; a click anywhere outside the popover closes it | | `keyboard` | `boolean` | `true` (arrow keys) | | `arrow` | `ArrowStyle` | `'caret'`; see [Arrows, spotlight and overlay](/customize/arrows-and-spotlight/) | | `spotlight` | `{ padding?, radius?, shape?, ring?, animate? }` | `8`, `10`, `'rounded'`, `'hairline'`, `true` | | `overlay` | `{ style?, color?, opacity?, blur? }` | `'dim'`, tinted ink, `0.52`, `4` | | `beacon` | `{ style?, text?, position?, offset?, size? }` | `'pulse'`, `'New'`, `'top-right'`, `0`, `10`; see [Beacons](/guides/beacons/) | | `mobile` | `{ layout?, card? }` | `layout`: `'auto'` (beside the target when the card fits, docked when it does not), `'float'` never docks, `'dock'` always does. `card`: `'stories'` (segmented progress on top, full-width main button), `'compact'` (a small card beside the element, pointing at it) or `'classic'`. See [Small screens](/guides/steps/#small-screens) | | `scroll` | `{ enabled?, behavior?, block? }` | `true`, `'auto'`, `'center'` | | `labels` | `Labels` | English defaults; `progress` supports `{current}`, `{total}`, `{current2}`, `{total2}` | | `theme` | `ThemeSpec` | a preset name, tokens, or `{ preset, ...tokens }` | | `appearance` | `'light' \| 'dark' \| 'auto'` | `'light'`; `auto` follows the system setting | | `template` | `string` | a built-in look (`spotlight`, `hint`, `announcement`), a [theme](/customize/themes/), or a template the app registered | | `minViewportWidth` | `number` | – ; in px. Below this viewport width the tour never shows, e.g. `768` to skip phones | ## Step [Section titled “Step”](#step) | Field | Type | Notes | | --------------- | ----------------------------------------- | ---------------------------------------------------------------------- | | `id` | `string` | required; unique within the tour | | `target` | `Target` | omit for a centred modal step | | `title`, `body` | `string` | | | `eyebrow` | `string` | overrides the tour’s eyebrow; `''` removes it | | `format` | `'text' \| 'markdown'` | markdown subset: bold, italic, code, links, paragraphs; never raw HTML | | `media` | `{ type: 'image' \| 'video', src, alt? }` | | | `placement` | `Placement` | `'auto'`, a side, or `side-start` / `side-end` | | `arrow` | `ArrowStyle` | per-step override | | `spotlight` | `SpotlightOptions` | per-step override | | `overlay` | `OverlayOptions` | per-step override | | `advance` | `Advance` | how the step completes; see [Steps](/guides/steps/) | | `interaction` | `'block' \| 'allow'` | default: `allow` for click/input advances, else `block` | | `condition` | `Condition` | skip the step when false | | `onMissing` | `'skip' \| 'wait' \| 'abort'` | default `skip` | | `waitFor` | `number` | ms to wait for the target; `wait` defaults to 3000 | | `route` | `string` | path pattern the step belongs to | | `buttons` | `{ back?, next?, skip?, close? }` | hide individual buttons | | `scroll` | `ScrollOptions` | per-step override | | `meta` | `Record` | | ## Target [Section titled “Target”](#target) A string is a CSS selector. An object is the portable form: | Field | Type | Notes | | ----------- | ---------- | ------------------------------------------------------------------ | | `name` | `string` | resolves to `[data-docent="name"]` on the web, a test id on native | | `selectors` | `string[]` | CSS fallbacks tried in order | | `native` | `string` | native id when it differs from `name` | | `within` | `string` | restrict the search to a container | | `nth` | `number` | pick among multiple matches | ## Advance [Section titled “Advance”](#advance) ```ts 'button' // Next button (default) { on: 'click', target? } // user clicks the target { on: 'input', target?, match? } // typed value matches a regex { on: 'event', name } // controller.notify(name) { on: 'element', target } // an element appears { on: 'delay', ms } ``` ## Trigger [Section titled “Trigger”](#trigger) ```ts { type: 'manual' } { type: 'auto', delay? } { type: 'route', pattern, delay? } { type: 'element', target, delay? } { type: 'event', name } { type: 'beacon', target?, open?, label? } // target defaults to the first step's; open: 'click' | 'hover' ``` ## Condition [Section titled “Condition”](#condition) ```ts { type: 'trait', key, op, value? } // op: eq neq gt gte lt lte in nin contains exists missing { type: 'route', pattern } { type: 'element', target, exists? } { type: 'tour', id, state } // not-started | in-progress | completed | skipped { type: 'all' | 'any', conditions } { type: 'not', condition } { type: 'custom', name, args? } // registered with the `custom` option ``` ## Theme [Section titled “Theme”](#theme) Sizes and times accept a CSS string or a number (`radius: 12` is `12px`, `duration: 180` is `180ms`). Tokens map to `--docent-*` custom properties: `background`, `foreground`, `muted`, `accent`, `accentForeground`, `radius`, `shadow`, `font`, `width`, `padding`, `overlay`, `overlayOpacity`, `duration`, `zIndex`, `connector`, `ring`, `beacon` (defaults to `accent`). All are CSS strings. ## Looks [Section titled “Looks”](#looks) ```ts type ArrowStyle = 'caret' | 'none' | 'line' | 'dashed' | 'dotted' | 'curve' | 'curve-dashed' | 'squiggle' | 'loop' | 'elbow' | 'sketch' | 'pin' type ProgressStyle = 'meter' | 'count' | 'ticks' | 'dots' | 'none' type SpotlightShape = 'rounded' | 'rect' | 'pill' | 'circle' type SpotlightRing = 'hairline' | 'none' | 'glow' | 'pulse' | 'dashed' | 'solid' type OverlayStyle = 'dim' | 'blur' | 'vignette' | 'none' type BeaconStyle = 'pulse' | 'dot' | 'ring' | 'badge' | 'none' type BeaconPosition = 'top-left' | 'top' | 'top-right' | 'right' | 'bottom-right' | 'bottom' | 'bottom-left' | 'left' | 'center' ``` ## Checking a tour [Section titled “Checking a tour”](#checking-a-tour) `validateTour` reports anything that does not match this page, in plain words, with the value that was probably meant: ```ts import { formatIssues, validateTour } from '@docentjs/dom/validate' const issues = validateTour(tour) // [{ level: 'error', path: 'steps[0].arrow', // message: '"curvy" is not one of: caret, none, line, …', suggestion: 'curve' }] console.log(formatIssues(issues)) ``` It catches unknown values, missing and misspelled fields, wrong types, out-of-range numbers and duplicate step ids. It says nothing about whether a step’s target is on the page, which the [devtools](/guides/devtools/#audit) Audit tab checks. **This already runs for you in development.** `createTour` and `createDocent` check each tour once and warn in the console. Production builds contain neither the check nor the code behind it, as long as your bundler sets `process.env.NODE_ENV`, which Vite, webpack, Next.js, Rollup and esbuild setups do. ### From the command line [Section titled “From the command line”](#from-the-command-line) ```sh npx @docentjs/cli validate "tours/*.json" ``` Checks tour files without a browser, which suits CI and tools that write tours. It exits 1 when anything is wrong. `--json` reports for other tools to read, and `docent schema tour-schema.json` writes the schema to a file for offline use. ```plaintext tours/welcome.json ✗ steps[0].arrow: "curvy" is not one of: caret, none, line, … Did you mean "curve"? ! steps[1].titel: Unknown field, which Docent will ignore. Did you mean "title"? 1 file checked: 1 error, 1 warning. ``` ### Warnings while you build [Section titled “Warnings while you build”](#warnings-while-you-build) Besides the schema check, Docent warns in development when something would otherwise fail in silence: * starting a tour id that does not exist, listing the ids it knows, * a step skipped because its target is not on the page, naming the step and the target. The same rule applies: production builds contain neither the checks nor their messages. The validate function is exported from `@docentjs/core/validate` and re-exported from `@docentjs/dom/validate`. `isValidTour(tour)` returns a boolean, `validateTheme(theme)` runs the same checks on a [theme](/customize/themes/) file, and `tourJsonSchema()` returns the JSON Schema published at [docentjs.dev/schema/tour-v1.json](https://docentjs.dev/schema/tour-v1.json). The documentation is also published as plain text for AI tools: see [Using Docent with AI](/reference/for-ai/). # Arrows, spotlight and overlay > Twelve arrow styles, four spotlight shapes, six rings and four overlay styles, all set in the tour JSON. Three things connect the popover to the page: the **arrow** between the popover and the element, the **spotlight** cut around the element, and the **overlay** that dims everything else. The defaults are a small caret, a rounded spotlight with a hairline ring, and a dimmed page. Each can change for a whole tour or for a single step, in plain JSON. Tip The [home page](/) has a playground where you can try every combination on one button. ## Arrows [Section titled “Arrows”](#arrows) ```ts options: { arrow: 'curve' } // for the whole tour steps: [{ id: 'save', arrow: 'pin' }] // or one step ``` | Style | Looks like | | -------------------------- | -------------------------------------------------------- | | `caret` | the default notch on the popover’s edge | | `none` | nothing | | `line`, `dashed`, `dotted` | a straight line with an arrowhead | | `curve`, `curve-dashed` | a curve that bows outward | | `squiggle` | a wavy line | | `loop` | a line with one small loop, like a hand-drawn annotation | | `elbow` | a right-angle connector with rounded corners | | `sketch` | a double stroke that reads as hand-drawn | | `pin` | a dotted line ending in a dot on the target | Connector styles (everything except `caret` and `none`) place the popover a little further away so the line has room, and draw in after the popover settles. Their code, about 2 kB, loads the first time a tour uses one, so tours with the default caret never download it. A connector always lands on the part of the target’s edge that faces the popover, so a sidebar or a full-width banner gets the same short, angled line as a button. When the target is too big to sit beside and the popover ends up on top of it, the line leaves the card sideways and lands on the stretch of edge the card does not cover; only a target the card hides completely falls back to the caret. LiveTen drawn arrows, one per stepPlay Share report ## Spotlight [Section titled “Spotlight”](#spotlight) ```ts options: { spotlight: { shape: 'circle', ring: 'pulse', padding: 10 } } ``` | Option | Values | | ------------------- | ---------------------------------------------------------------- | | `shape` | `rounded` (default), `rect`, `pill`, `circle` | | `ring` | `hairline` (default), `none`, `glow`, `pulse`, `dashed`, `solid` | | `padding`, `radius` | pixels; radius applies to `rounded` | `circle` circumscribes the target, so it suits buttons and icons more than wide cards. `pulse` repeats gently to draw the eye and stops for users who prefer reduced motion. LiveShapes, then ringsPlay Spotlight me ## Overlay [Section titled “Overlay”](#overlay) ```ts options: { overlay: { style: 'blur', blur: 6, opacity: 0.4 } } ``` | Style | Effect | | ---------- | ----------------------------------------------------------------- | | `dim` | a tinted scrim (default) | | `blur` | the scrim plus a soft blur of the page | | `vignette` | clear near the target, darker toward the edges | | `none` | no scrim, and the page stays usable, for lighter hint-style tours | `color` and `opacity` tint the scrim; `blur` sets the blur radius. With `none`, connectors and the ring switch to the accent color so they stay visible on the page. LiveBlur, vignette, and no overlayPlay Export ## Three looks under one name [Section titled “Three looks under one name”](#three-looks-under-one-name) Setting an arrow, a spotlight and an overlay for each tour gets repetitive. Three built-in looks bundle the common combinations, and a tour picks one by name: ```ts options: { template: 'hint' } ``` | Name | What it does | Suits | | -------------- | ------------------------------------------------------------------------------------ | ------------------------------ | | `spotlight` | the default: dimmed page, soft cutout, small caret | walking someone through a task | | `hint` | no dimming so the page stays usable, a glowing ring, a curved arrow, a narrower card | a tip beside one feature | | `announcement` | the page blurs away, no arrow, no ring, a wider card | something to read, not to do | They are ordinary [templates](/customize/slots-and-templates/#templates), so registering a template of the same name replaces the built-in one, and anything the tour or the step sets still wins. ## Where to set it [Section titled “Where to set it”](#where-to-set-it) Settings stack. Each layer overrides the one before it: 1. renderer options, for every tour: `createTour(tour, { renderer: { arrow: 'curve' } })`, 2. a [template](/customize/slots-and-templates/#templates), 3. the tour’s `options`, 4. the step itself. So a tour can use curved arrows throughout, and one step can switch to a pin for a small icon. Colors come from the `connector` and `ring` [theme tokens](/customize/theming/#tokens). The [devtools](/guides/devtools/) Edit tab switches all of these live on a running tour. # Headless mode > Draw the whole popover yourself and keep everything else Docent does. When slots are not enough, for example when the popover must be your design system’s own component, replace it entirely. Docent keeps doing the hard parts: * the overlay and spotlight, including blocking clicks on the page, * positioning, flipping, keeping on screen, and docking on small screens, * scrolling the element into view and moving the popover out from under sticky headers, * Escape, arrow keys, keeping Tab inside your popover, and returning focus afterwards, * state, saved progress, events and hooks. You only render what goes in the box. ## The render function [Section titled “The render function”](#the-render-function) ```ts createTour(tour, { renderer: { headless: { render(ctx, container) { const card = document.createElement('div') card.className = 'my-card' const title = document.createElement('h3') title.textContent = ctx.step.title ?? '' const next = document.createElement('button') next.textContent = ctx.isLast ? 'Done' : 'Next' next.onclick = ctx.actions.next card.append(title, next) container.append(card) return () => card.remove() // cleanup, run before the next step }, }, }, }) ``` `render` is called for every step. It receives: * **`ctx`**: the `step`, the `tour`, the step’s `index`, `progress` (`current` and `total`), the flags `isFirst`, `isLast` and `canGoBack`, and `actions`: `next()`, `back()`, `skip()` and `goTo(stepIdOrIndex)`. Wire your close button to `skip()`. * **`container`**: an element in your page that Docent positions next to the target. Put your popover inside it. Return a function to clean up before the next step. ## Drawing your own arrow [Section titled “Drawing your own arrow”](#drawing-your-own-arrow) The container tells you where it ended up. `data-side` is `top`, `right`, `bottom`, `left`, `center` or `sheet`, and the `--docent-arrow` custom property is the arrow’s offset along that side, in pixels. ```css [data-docent-popover][data-side='bottom'] .my-arrow { top: -6px; left: calc(var(--docent-arrow) - 6px); } ``` Tip In React, Vue and Svelte you do not need `render` at all. Pass your component and the adapter mounts it into the container for you. See [React](/frameworks/react/#your-own-popover), [Vue](/frameworks/vue/#your-own-popover) and [Svelte](/frameworks/svelte/#your-own-popover). # Slots and templates > Replace one part of the popover with your own content, and bundle a look under a name that tours can pick. Check the fields first The two things most designs reach for — a small line above the title, and a different progress counter — are [fields](/customize/themes/), not slots: `eyebrow`, `progress` and `count`. Staying in JSON keeps the look installable, shareable and editable by a tool. Use a slot when you need markup the library has no field for, like a diagram. Customization comes in levels. Use the lowest one that does the job: | You want to | Use | | --------------------------------------- | ------------------------------------------ | | change colors, corners, width or font | [theme tokens](/customize/theming/) | | restyle a part a little | [CSS parts](/customize/theming/#css-parts) | | replace one part with your own content | **slots**, on this page | | reuse a whole look across tours by name | **templates**, on this page | | draw the entire popover yourself | [headless mode](/customize/headless/) | ## Slots [Section titled “Slots”](#slots) A slot is one region of the built-in popover: `header`, `title`, `close`, `body`, `media`, `footer`, `progress` or `buttons`. Give the renderer a function for a slot and it is called for every step. The function receives the render context and returns one of: * a DOM `Node` or a string, to replace the region, * `null`, to remove the region, * `undefined`, to keep the default. This example replaces the step counter with dots and removes the close button: ```ts createTour(tour, { renderer: { slots: { progress: (ctx) => { const dots = document.createElement('div') dots.className = 'dots' for (let i = 1; i <= ctx.progress.total; i++) { const dot = document.createElement('i') if (i === ctx.progress.current) dot.className = 'on' dots.appendChild(dot) } return dots }, close: () => null, }, }, }) ``` Your content is projected through native shadow DOM slots, so it stays in your page’s DOM. Your CSS styles it, and your framework’s event handling keeps working. ## Templates [Section titled “Templates”](#templates) A template bundles a theme, slots and CSS under a name. Register templates in code, then let each tour pick one by name in its JSON: ```ts createTour(tour, { renderer: { templates: { card: { theme: { radius: '16px' }, slots: { progress: dots }, css: '.popover { border: 1px solid rgba(124, 58, 237, 0.35) }', }, }, }, }) ``` ```ts // in the tour JSON options: { template: 'card' } ``` This split is deliberate. Code stays in your app, while the JSON only says “use card”, so a builder or an API can choose looks without shipping functions. Set the renderer’s `template` to apply one when a tour names none. Three names are built in: `spotlight`, `hint` and `announcement`. See [three looks under one name](/customize/arrows-and-spotlight/#three-looks-under-one-name). Registering a template with one of those names replaces it. Templates can also set `arrow`, `spotlight`, `overlay`, `eyebrow`, `progress` and `count`, so a “hint” template could use no overlay and a pin arrow everywhere. A template with no `slots` is a [theme](/customize/themes/) — pure data, so it can be published and installed as one file. `renderer.template` takes one directly: ```ts import theme from './docent-theme.json' createTour(tour, { renderer: { template: theme } }) ``` The type is `DocentTheme`, exported from `@docentjs/dom` and each framework package. LiveA template with CSS, picked by name in the tourPlay OneTwo Note Slots are functions, which cannot travel in JSON, so this demo shows only the theme and CSS parts of a template. In React, Vue and Svelte, a custom popover component is usually simpler than slots; see the [framework pages](/frameworks/react/). # Themes > Install a ready-made look with one command. A theme is one JSON file, so there is nothing else to wire up. A theme is one JSON file. It carries colors, spacing, the shape of the progress counter, the arrow, the spotlight and the scrim — everything about how a tour looks, and nothing about what it does. Because it is only data, installing one writes one file into your project. There is no package to add at runtime, no build step, and nothing to keep in sync with your framework. ## Install [Section titled “Install”](#install) 1. **Add a theme.** Pick one from the [gallery](#the-gallery) below: ```sh npx @docentjs/cli theme add ledger ``` This writes `docent-theme.json`, after checking it. Use `--out` to put it somewhere else, and `docent theme list` to see every theme. 2. **Hand it to the renderer.** One line, wherever you already set up Docent: * Vanilla ```ts import { createTour } from '@docentjs/dom' import theme from './docent-theme.json' createTour(tour, { renderer: { template: theme } }) ``` * React ```tsx import { DocentProvider } from '@docentjs/react' import theme from './docent-theme.json' ``` * Vue ```ts import { provideDocentDefaults } from '@docentjs/vue' import theme from './docent-theme.json' provideDocentDefaults({ renderer: { template: theme } }) ``` * Svelte ```ts import { useDocent } from '@docentjs/svelte' import theme from './docent-theme.json' const docent = useDocent({ tours, renderer: { template: theme } }) ``` 3. **That is it.** Every tour in the app now uses the theme. ### From anywhere [Section titled “From anywhere”](#from-anywhere) A theme is just a URL away, so anyone can publish one. The CLI checks it before writing anything, and refuses a file that is not a valid theme: ```sh npx @docentjs/cli theme add https://example.com/my-theme.json ``` The same check is available in code as `validateTheme(theme)` from `@docentjs/dom/validate`, for themes you load at runtime. Prefer not to use the CLI? Copy the JSON from any card below into your project instead. It is the same file. Several themes at once Register them by name and let each tour choose: `renderer: { templates: { calm, loud } }`, then `options: { template: 'loud' }` in the tour JSON. Handy when a release note should look different from an onboarding tour. ## Make it yours [Section titled “Make it yours”](#make-it-yours) Open the file and change it. Every field is documented in [theming](/customize/theming/) and [arrows and spotlight](/customize/arrows-and-spotlight/); the ones a theme uses most: | Field | What it does | | ----------- | ---------------------------------------------------------------------------------------------------------------------- | | `theme` | colors, radius, width, padding, font, shadow, scrim | | `eyebrow` | a small line above every title. `{tour}` becomes the tour’s name | | `progress` | `meter`, `count`, `ticks`, `dots` or `none` | | `arrow` | `caret`, `none`, or one of the ten drawn connectors | | `spotlight` | padding, radius, shape and ring around the target | | `overlay` | `dim`, `blur`, `vignette` or `none` | | `mobile` | on phones: `layout` (`auto`, `float`, `dock`) and `card` (`stories`, `compact`, `classic`); a tour’s own `mobile` wins | | `css` | anything the fields above do not cover, scoped to the popover | A tour can override any of it, so one tour can go its own way without a second theme: ```json { "options": { "template": "ledger", "progress": "dots", "eyebrow": "New in 2.4" } } ``` ## The gallery [Section titled “The gallery”](#the-gallery) Press **Preview** to run a real tour with that theme. These are live — the card is the library rendering, not a picture of it. Bloomdots progress · squiggle arrowPreview PublishShareSettings `npx @docentjs/cli theme add bloom`Copy bloom.jsonCopy ``` { "name": "Bloom", "theme": { "background": "oklch(99% 0.008 330)", "foreground": "oklch(26% 0.05 330)", "muted": "oklch(58% 0.04 330)", "accent": "oklch(58% 0.21 350)", "accentForeground": "oklch(99% 0.008 350)", "connector": "oklch(58% 0.21 350)", "ring": "oklch(78% 0.16 350)", "radius": 20, "width": 336, "padding": "20px 22px 16px", "shadow": "0 2px 4px oklch(40% 0.12 330 / 0.1), 0 28px 60px -28px oklch(40% 0.14 330 / 0.6)", "duration": 260 }, "progress": "dots", "arrow": "squiggle", "spotlight": { "padding": 10, "radius": 16, "ring": "pulse", "animate": true }, "overlay": { "color": "oklch(30% 0.09 330)", "opacity": 0.38 }, "css": ".title { font-size: 19px; letter-spacing: -0.015em; } .button { border-radius: 999px; padding-inline: 14px; }" } ``` Broadsheetcount progress · eyebrowPreview PublishShareSettings `npx @docentjs/cli theme add broadsheet`Copy broadsheet.jsonCopy ``` { "name": "Broadsheet", "theme": { "background": "oklch(98.6% 0.006 85)", "foreground": "oklch(21% 0.012 60)", "muted": "oklch(52% 0.014 60)", "accent": "oklch(45% 0.14 28)", "accentForeground": "oklch(98% 0.008 85)", "connector": "oklch(45% 0.14 28)", "radius": 3, "width": 360, "padding": "18px 20px 16px", "duration": 190 }, "eyebrow": "{tour}", "progress": "count", "spotlight": { "padding": 8, "radius": 3, "ring": "hairline" }, "overlay": { "color": "oklch(24% 0.03 60)", "opacity": 0.46 }, "css": ".title { font-family: ui-serif, Georgia, 'Times New Roman', serif; font-size: 21px; letter-spacing: -0.01em; } .eyebrow { letter-spacing: 0.14em; } .footer { margin-top: 20px; padding-top: 12px; border-top: 1px solid color-mix(in oklch, var(--docent-fg) 14%, transparent); }" } ``` Carbonticks progressPreview PublishShareSettings `npx @docentjs/cli theme add carbon`Copy carbon.jsonCopy ``` { "name": "Carbon", "theme": { "background": "oklch(13% 0.01 285)", "foreground": "oklch(99% 0.002 285)", "muted": "oklch(88% 0.008 285)", "accent": "oklch(90% 0.18 100)", "accentForeground": "oklch(13% 0.01 285)", "connector": "oklch(90% 0.18 100)", "ring": "oklch(90% 0.18 100)", "radius": 4, "width": 340, "padding": "18px", "shadow": "none" }, "progress": "ticks", "spotlight": { "padding": 6, "radius": 2, "ring": "solid" }, "overlay": { "opacity": 0.8 }, "css": ".popover { box-shadow: 0 0 0 2px var(--docent-accent); } .marks i { height: 3px; }" } ``` Consolecount progress · eyebrowPreview PublishShareSettings `npx @docentjs/cli theme add console`Copy console.jsonCopy ``` { "name": "Console", "theme": { "background": "oklch(24.5% 0.016 60)", "foreground": "oklch(94% 0.008 60)", "muted": "oklch(72% 0.012 60)", "accent": "oklch(74% 0.165 58)", "accentForeground": "oklch(19% 0.04 60)", "connector": "oklch(74% 0.165 58)", "ring": "oklch(74% 0.165 58)", "radius": 8, "width": 344, "padding": "16px 16px 12px", "shadow": "0 24px 56px -24px oklch(8% 0.02 60 / 0.95)", "duration": 200 }, "eyebrow": "{tour}", "progress": "count", "count": "{current2}/{total2}", "spotlight": { "ring": "hairline", "padding": 8, "radius": 6, "animate": true }, "overlay": { "color": "oklch(12% 0.014 60)", "opacity": 0.66 }, "css": ".count { font-family: ui-monospace, SFMono-Regular, Menlo, monospace; letter-spacing: 0.02em; } .button { border-radius: 6px; }" } ``` Ledgerticks progress · eyebrowPreview PublishShareSettings `npx @docentjs/cli theme add ledger`Copy ledger.jsonCopy ``` { "name": "Ledger", "theme": { "background": "oklch(99.3% 0.005 95)", "foreground": "oklch(24% 0.014 95)", "muted": "oklch(58% 0.011 95)", "accent": "oklch(42% 0.082 155)", "accentForeground": "oklch(98% 0.012 155)", "connector": "oklch(42% 0.082 155)", "ring": "oklch(98% 0.01 95)", "radius": 12, "width": 352, "padding": "18px 18px 14px", "shadow": "0 1px 2px oklch(24% 0.014 95 / 0.08), 0 26px 52px -26px oklch(24% 0.014 95 / 0.55)", "duration": 220 }, "eyebrow": "{tour}", "progress": "ticks", "count": "{current2} / {total2}", "spotlight": { "padding": 10, "radius": 10, "animate": true }, "overlay": { "color": "oklch(32% 0.03 95)", "opacity": 0.44 } } ``` Nocturnedots progress · eyebrow · curve arrowPreview PublishShareSettings `npx @docentjs/cli theme add nocturne`Copy nocturne.jsonCopy ``` { "name": "Nocturne", "theme": { "background": "oklch(23% 0.013 265)", "foreground": "oklch(95% 0.004 265)", "muted": "oklch(74% 0.008 265)", "accent": "oklch(95% 0.004 265)", "accentForeground": "oklch(14.5% 0.008 265)", "connector": "oklch(68% 0.13 48)", "ring": "oklch(68% 0.13 48)", "radius": 10, "width": 340, "padding": "16px 16px 12px", "shadow": "0 26px 60px -26px oklch(6% 0.01 265 / 0.95)", "duration": 200 }, "eyebrow": "{tour}", "progress": "dots", "arrow": "curve", "spotlight": { "padding": 8, "radius": 9, "ring": "glow", "animate": true }, "overlay": { "color": "oklch(10% 0.01 265)", "opacity": 0.68 } } ``` Quietno progress · curve arrow · no scrimPreview PublishShareSettings `npx @docentjs/cli theme add quiet`Copy quiet.jsonCopy ``` { "name": "Quiet", "theme": { "background": "oklch(99.4% 0.003 285)", "foreground": "oklch(23% 0.018 285)", "muted": "oklch(56% 0.014 285)", "accent": "oklch(26% 0.02 285)", "radius": 8, "width": 300, "padding": "14px 16px 12px", "shadow": "0 1px 2px oklch(23% 0.02 285 / 0.08), 0 6px 16px -6px oklch(23% 0.02 285 / 0.14)" }, "progress": "none", "arrow": "curve", "spotlight": { "ring": "glow", "padding": 6, "radius": 8 }, "overlay": { "style": "none" }, "css": ".title { font-size: 16px; }" } ``` ## Writing your own [Section titled “Writing your own”](#writing-your-own) Start from the closest theme, change the tokens, and keep going until the fields run out. If you reach something no field covers, `css` is the next step, and [slots](/customize/slots-and-templates/) after that — but a theme that needs code is no longer something you can publish, share or let a tool edit, so it is worth staying in JSON as long as you can. Theme or tour? Put it in the **theme** when it should be true of every tour in the product, and in the **tour** when it belongs to that one tour. Both accept the same fields. # Theming > Match the popover to your product with a handful of tokens, a preset, or your own CSS. Docent looks finished out of the box, and most teams change one thing: the accent color. This page goes from that one change to full control. Looking for a ready-made look instead? The [theme gallery](/customize/themes/) has several to copy, and each one is a single JSON file. ## The default look [Section titled “The default look”](#the-default-look) Ink on paper: a near-white card, near-black text and an ink Next button, all tinted very slightly toward violet, with hairline edges instead of heavy borders. It is meant to sit quietly in any product without competing with its brand. * **Your font.** The popover inherits your site’s font, so tours feel native. Set the `font` token only if you want something else. * **Light by default.** Dark mode never switches on from the operating system setting, because a dark popover on a light site looks broken. On a dark product, use the `dark` preset or your own tokens. ## Tokens [Section titled “Tokens”](#tokens) Tokens are a few named values, such as colors and sizes. They are part of the tour JSON, so they travel with the tour and a builder can set them. ```ts defineTour({ id: 'welcome', options: { theme: { accent: '#0d9488', radius: '8px', width: '320px' }, }, steps: [ … ], }) ``` | Token | Controls | | ----------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `accent`, `accentForeground` | the Next button’s background and text | | `background`, `foreground`, `muted` | the card, its main text, and secondary text | | `radius`, `width`, `shadow` | the card’s corners, width and shadow | | `padding` | the space inside the card; any CSS padding value | | `font` | the font family, if not your site’s | | `overlay`, `overlayOpacity` | the backdrop, but prefer the [overlay options](/customize/arrows-and-spotlight/#overlay), which win when both are set | | `ring`, `connector` | the spotlight ring and drawn arrows | | `duration` | how long transitions take | | `zIndex` | the stacking order, if something of yours sits above tours | Each token becomes a `--docent-*` CSS custom property, for example `--docent-accent`. Colors take any CSS color. Sizes and times take a CSS string or a plain number, so `radius: 12` and `radius: '12px'` are the same thing, and `duration: 180` means 180 ms. The button text looks after itself Set `accent` alone and Docent picks the readable text color for it. A dark accent gets light text, a light one such as yellow gets dark text. Set `accentForeground` when you want a specific color. LiveTokens set in the tour JSONPlay Target ## Presets [Section titled “Presets”](#presets) Four ready-made themes ship in a separate entry point, so they add nothing to your bundle unless you import one. Name one in the tour itself, which keeps the JSON self-contained: ```ts options: { theme: 'dark' } // a preset options: { theme: { preset: 'dark', accent: '#7c3aed' } } // a preset, then your changes ``` Or import it in code, for every tour in the app: ```ts import { dark } from '@docentjs/dom/themes' createTour(tour, { renderer: { theme: dark } }) ``` Presets named in JSON load on demand, so tours that don’t name one never download them. | Preset | For | | ---------- | ------------------------------------- | | `light` | the default, spelled out, to build on | | `dark` | dark products | | `minimal` | flatter and tighter | | `contrast` | maximum legibility | In React, Vue and Svelte, import presets from `@docentjs/react/themes`, `@docentjs/vue/themes` or `@docentjs/svelte/themes`. LiveThe contrast presetPlay Target ## Light, dark, or the reader’s choice [Section titled “Light, dark, or the reader’s choice”](#light-dark-or-the-readers-choice) ```ts options: { appearance: 'auto' } ``` | Value | Effect | | ------- | ------------------------------------------------------------------ | | `light` | the default surface | | `dark` | the dark surface | | `auto` | follows the reader’s system setting, and switches with it mid-tour | Your own tokens still apply on top, so `{ appearance: 'auto', theme: { accent: '#7c3aed' } }` keeps your brand color on both surfaces. ## Where themes can be set [Section titled “Where themes can be set”](#where-themes-can-be-set) Themes stack. Each layer overrides only the tokens it sets, from lowest to highest: 1. the renderer’s `theme`, for every tour in your app, 2. the surface chosen by `appearance`, 3. the template’s theme, when the tour names a [template](/customize/slots-and-templates/#templates), 4. the tour’s `options.theme`. A common setup is a preset on the renderer, your brand accent on top, and occasional per-tour tweaks in the JSON. ## CSS parts [Section titled “CSS parts”](#css-parts) For anything tokens do not cover, style the popover’s parts from your own stylesheet with `::part()`: ```css [data-docent-host]::part(title) { letter-spacing: -0.01em; } [data-docent-host]::part(button-next) { border-radius: 999px; } ``` The parts are `overlay`, `ring`, `connector`, `popover`, `arrow`, `header`, `title`, `close`, `body`, `media`, `footer`, `progress`, `meter`, `count`, `buttons`, `button`, `button-next`, `button-back` and `button-skip`. Tip To add CSS inside the shadow root instead, use the renderer’s `css` option or a template’s `css`. That lets you target any element, not only named parts. # Examples > Four complete apps, each with its own design language and a full Docent setup — running in the browser, with the source on GitHub. Four small products rather than four demos. Each one is a plausible slice of a real interface with its own typography, palette and layout, so you can see what a tour looks like when it has to live inside someone else’s brand instead of next to it. Every app runs the manager (`useDocent` / `createDocent`) alongside one tour the app draws itself, so both halves of the API appear in all four. [![Ledgerline, a receivables dashboard, with a tour popover pointing at the open ledger.](/_astro/react.C7eXfrdz_1Smvpa.webp)](/examples/react/) [LedgerlineReact](/examples/react/) [Receivables for a design studio. An auto trigger that fires once per person, steps that hand control back to the user, a step that waits for a drawer to mount, traits gating a release note, and three templates.](/examples/react/) [![Cinder, a dark incident console, with a tour popover explaining the alert queue.](/_astro/vue.CzZbe5Gg_Z1OntWf.webp)](/examples/vue/) [CinderVue](/examples/vue/) [An on-call incident console. A tour triggered by an element appearing rather than a timer, a step that waits for a checklist to be revealed, and a dark surface with brand tokens layered over it.](/examples/vue/) [![Pressroom, a newsroom run of show, with a serif tour popover and a roman numeral step counter.](/_astro/svelte.CQzSabdF_Z1kp1HE.webp)](/examples/svelte/) [PressroomSvelte](/examples/svelte/) [A newsroom run of show. The buttons slot replaced with the paper’s own controls, a folio counter in roman numerals, and a hand-drawn connector for the galley note.](/examples/svelte/) [![Nocturne, a radio playout desk, with a tour popover under the waveform.](/_astro/vanilla.BxxXxU9l_Z1BOyMs.webp)](/examples/vanilla/) [NocturneNo framework](/examples/vanilla/) [An overnight radio playout desk. Templates, slots and a headless popover built with document.createElement, plus a custom condition that keeps a tour from running when the studio is off air.](/examples/vanilla/) ## Linking into a tour [Section titled “Linking into a tour”](#linking-into-a-tour) Each app reads a `start` parameter and runs that tour on load, so a link can drop someone straight into the thing you are describing: ```plaintext /examples/vue/?start=cinder-postmortem /examples/vanilla/?start=nocturne-handover ``` ## Reading the source [Section titled “Reading the source”](#reading-the-source) Every app keeps its tours in `src/tours.ts`, its renderer defaults and templates in `src/tour-theme.ts`, and its app-drawn popover in `src/TourCard.*`. Those three files are the whole integration; the rest is an ordinary app. [Source on GitHub](https://github.com/FgrReloaded/docentjs/tree/main/examples)All four apps, plus a README saying which Docent features each one covers. [Run them locally](https://github.com/FgrReloaded/docentjs/blob/main/examples/README.md)pnpm build, then one dev server per example. # Beacons > A small mark on an element that opens a tip, or a whole tour, when the reader clicks or hovers it. A tour interrupts. A beacon waits. It puts a small mark on an element, such as a new button, and opens a tip only when the reader asks for it by clicking or hovering the mark. A beacon is a tour with a `beacon` trigger. Everything else about it is an ordinary tour, so conditions, frequency, versions, events, validation and the devtools all work the same way. ```ts import { createDocent, defineTour } from '@docentjs/dom' const exportTip = defineTour({ id: 'export-tip', trigger: { type: 'beacon' }, steps: [ { id: 'tip', target: { name: 'export' }, title: 'Export to CSV', body: 'Download any table as a spreadsheet.' }, ], }) createDocent({ tours: [exportTip] }) ``` That is all the setup there is. The mark sits on the first step’s target, takes the theme’s accent colour, and goes away once it has been read. LiveClick the badgeShow again FilterExport Note Beacons are drawn by the [tour manager](/guides/manager/): `createDocent`, or `useDocent` in React, Vue and Svelte. A tour started on its own with `createTour` has no trigger, so it has no beacon either. ## What a beacon’s tip does by default [Section titled “What a beacon’s tip does by default”](#what-a-beacons-tip-does-by-default) A tip opened from a beacon should feel light, so a beacon tour starts from these defaults. Anything the tour sets itself still wins. | Default | Change it with | | ----------------------------------------- | ------------------------------------ | | No scrim; the page stays usable | `options.overlay: { style: 'dim' }` | | A click outside the popover closes it | `options.closeOnOutsideClick: false` | | With one step, no step counter | `options.progress` | | With one step, the button says **Got it** | `options.labels: { done: '…' }` | A beacon can also open a tour of several steps. It behaves like any other tour, with a counter, and the beacon stays on the page until the tour ends. ## Click or hover [Section titled “Click or hover”](#click-or-hover) ```ts trigger: { type: 'beacon' } // opens on click (the default) trigger: { type: 'beacon', open: 'hover' } // opens on hover, or on keyboard focus ``` A hover beacon shows its tip as a preview: * The tip opens after the pointer rests on the mark for 200 ms, so passing over it does nothing. * It closes 250 ms after the pointer leaves both the mark and the tip, so the reader can move into the tip to use a link or a button. * Clicking inside the tip, or clicking the mark, keeps it open until it is closed. * A tour of more than one step always stays open until it is closed. * Tabbing to the mark shows the preview. **Enter** moves focus into it, and **Escape** closes it and returns focus to the mark. * A preview never takes focus, so it cannot interrupt someone who is typing. * On a touch screen, which cannot hover, a tap opens it. Closing a preview counts as reading it. With the default `frequency: 'once'`, the mark goes away afterwards. LiveHover the dotShow again Overdue: £21,480 ## Tooltips [Section titled “Tooltips”](#tooltips) With `style: 'none'` there is no mark. The element itself opens the tip when it is hovered or focused, which makes a plain tooltip: ```ts trigger: { type: 'beacon', open: 'hover' }, options: { frequency: 'always', beacon: { style: 'none' } }, ``` Set `frequency: 'always'` so the tooltip keeps working after the first time. A tooltip has nothing to tap, so it does not open on touch screens. Keep what it says available some other way on phones. LiveHover the planShow again Studio plan ## Looks [Section titled “Looks”](#looks) `options.beacon` sets how the mark looks. Every field is optional. | Field | Values | Default | | ---------- | -------------------------------------------------------------------------------------------------- | ----------- | | `style` | `pulse`, `dot`, `ring`, `badge`, `none` | `pulse` | | `text` | the text of a `badge` | `New` | | `position` | `top-left`, `top`, `top-right`, `right`, `bottom-right`, `bottom`, `bottom-left`, `left`, `center` | `top-right` | | `offset` | px away from the target’s centre, or an exact shift `{ x, y }` | `0` | | `size` | the dot’s diameter, in px | `10` | The mark is centred on its position, so at `top-right` it sits half over the element’s corner. A negative `offset` moves it inward. Its colour is the `beacon` theme token, which falls back to `accent`: ```ts options: { theme: { beacon: '#d9442b' } } ``` As with the other [looks](/customize/arrows-and-spotlight/), the renderer’s `beacon` option sets a default for every tour, a [theme](/customize/themes/) or template can carry a `beacon` block, and a tour’s own `options.beacon` wins over both. Beacons also follow `appearance` and the tour’s theme, so a dark tour gets a mark that suits it. ## Where the mark goes [Section titled “Where the mark goes”](#where-the-mark-goes) The mark sits on the first step’s target. To put it somewhere else, give the trigger its own target: ```ts trigger: { type: 'beacon', target: { name: 'help-menu' } } ``` It follows the element through scrolling, resizing and layout changes. It is hidden while the element is missing, has no size, or is scrolled off screen, and it appears again when the element comes back. While a tour is running, every other beacon is hidden, so nothing competes with the tour. The beacon that opened a tour stays until that tour ends. ## Who sees it, and how often [Section titled “Who sees it, and how often”](#who-sees-it-and-how-often) Beacons follow the usual [triggers and conditions](/guides/triggers-and-conditions/) rules. The mark shows only while the tour’s conditions hold and its frequency allows it: | `frequency` | The mark goes away | | ----------------- | ------------------------------------------------------------------------ | | `once` (default) | once the tip has been opened and closed, however it closed | | `until-completed` | once the reader finishes the tour; closing it early brings the mark back | | `always` | never; use this for standing help and tooltips | Bump the tour’s `version` to show a beacon again to people who have already read it. ## Accessibility [Section titled “Accessibility”](#accessibility) * The mark is a real button. Screen readers announce `trigger.label`, or the tour’s `name` when there is no label, and whether the tip is open. * The button takes taps and clicks across 44 × 44 px, whatever size the mark is drawn. * With reduced motion, the pulse is replaced by a still ring. ## Events [Section titled “Events”](#events) The manager reports two more events to your `sink`: | Event | When | | --------------- | ------------------------------------------------ | | `beacon:shown` | the mark appears for the first time on this page | | `beacon:opened` | the reader opens it; `via` is `click` or `hover` | With `tour:completed` and `tour:skipped`, that makes a funnel for each beacon: seen, opened, read. # Devtools > See why a tour is or isn't showing, replay steps, and simulate users while you build. `@docentjs/devtools` is a panel for development. It is a separate package you load only in development, so production bundles never include it. ```sh pnpm add -D @docentjs/devtools ``` Render the component anywhere. It shows nothing on the page itself, loads the panel only in development, and production builds drop it entirely. ```tsx // React import { DocentDevtools } from '@docentjs/devtools/react' const docent = useDocent({ tours }) return ``` ```vue ``` ```svelte ``` `docent` accepts the handle from `useDocent()` or a manager from `createDocent()`. Props `open` and `shortcut` match the `mount()` options. Production detection relies on your bundler replacing `process.env.NODE_ENV`, which Vite, webpack, Next.js, Rollup and esbuild setups do. Without a framework, mount it yourself inside a development-only branch: ```ts import { createDocent } from '@docentjs/dom' const docent = createDocent({ tours }) if (import.meta.env.DEV) { import('@docentjs/devtools').then(({ mount }) => mount(docent)) } ``` Open it with the **Docent** button in the corner or **Alt+Shift+D**. Pick where the panel sits with the three layout buttons in its header: left, bottom or right. Drag its edge to resize it; each layout remembers its own size. In windows narrower than 640 px, such as a phone or a narrow device preview, a side panel would cover the whole page, so it docks at the bottom instead and goes back to your choice when the window is wider. A bar at the top always shows the running tour and step, with Back, Next, Edit this step and Stop. ## Tours [Section titled “Tours”](#tours) Every tour the manager knows, with a verdict and the reason: | Verdict | Meaning | | -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | running | showing now | | eligible | conditions and frequency pass and the trigger holds; it should show, or is queued. For a [beacon](/guides/beacons/), the beacon is on the page waiting to be opened | | waiting | eligible, but its trigger has not fired: another route, an element not on the page yet, an event not tracked | | blocked | a condition failed, or frequency says this user has seen it | | manual | no trigger; start it from code | Expand a tour for its trigger, frequency, and each condition with the user’s actual values, for example `trait plan eq "trial" (user has "pro")`. Each step shows whether its target is on this page, which selector matched, or what was tried. Hover a step to outline its target. **Play** starts a tour regardless of its rules, **▶** starts from a specific step, **Reset progress** makes it show again, and **Copy JSON** copies the definition. ## Edit [Section titled “Edit”](#edit) A live editor for any tour. Changes apply to the running tour as you type, so you see the popover, spotlight and arrow update in place. * **Steps**: reorder, duplicate, add and delete steps; preview from any step. * **Step fields**: title, body, placement, target, how the step advances, what happens when the target is missing, and per-step arrow, spotlight shape and ring. * **Target picker**: click **Pick** and then any element on the page. The panel suggests selectors ranked by stability, preferring `data-docent` names and ids over generated class names and structural paths, and tells you when one looks fragile. * **Tour**: trigger, frequency, progress, close and keyboard options. With **From a beacon**, also how it opens, the beacon’s style, position, size, offset, badge text and label, and its colour under **Theme**. The beacon on the page redraws as you edit. * **Look**: arrow style, overlay style and blur, spotlight shape, ring, padding and radius, and the **Phone layout** and **Phone card** used below 480 px. See [Arrows, spotlight and overlay](/customize/arrows-and-spotlight/) and [Small screens](/guides/steps/#small-screens). * **Theme**: start from a preset and adjust colors, radius, width and font. ### Pocket: the page at phone size [Section titled “Pocket: the page at phone size”](#pocket-the-page-at-phone-size) The phone button in the panel’s header opens **Pocket**: your page, loaded in a phone-sized frame beside the panel, with the selected tour running in it. The page inside lays out at the phone’s own width, so its phone styles, hidden sidebars and menus are what you see, and the tour uses its phone layout. * **Sizes**: 360, 390 and 430 px (common phones) and 480 px, where tours switch to their phone layout. The rotate button turns the phone sideways. The phone is drawn at its true size, never scaled; in a short window it is shorter instead (marked *fitted*), like a phone with its browser bars showing. * **Edits show up in the phone** as you make them, like they do on the page. * **Choosing a step** in the Edit tab takes the phone there, and **Preview step** starts it in the phone. The bar at the top of the panel follows the phone while Pocket is open. A tour already running on the page is left as it is, under Pocket. The frame loads the same page, so it needs the page to allow being framed by itself (the default on development servers). ### Keeping your edits [Section titled “Keeping your edits”](#keeping-your-edits) Edits apply to the running page, not to your source files. To keep them, **Copy JSON** or **Download** the tour and put it back in your code. If your tours live in `.json` files, the download can replace the file as it is: ```ts import welcomeJson from './tours/welcome.tour.json' export const welcome = defineTour(welcomeJson) ``` Until then, unsaved edits are kept in this browser, so a reload or a hot update does not lose them. The Edit tab tells you when edits were restored from an earlier session. It also warns you when the tour’s code has changed since you edited it, so you can review the edits or discard them. Once your code matches the edits, the saved copy is dropped. **Discard edits** always returns to the version in your code. ## Simulate [Section titled “Simulate”](#simulate) Change the user id and traits and apply them, which calls `identify()` so targeting re-evaluates immediately. Fire an event as if your app called `track()`. Navigate to another path without a reload. ## Events [Section titled “Events”](#events) A live timeline of every lifecycle event: tours started, steps shown and completed, targets missing. Filter it, and copy it as JSON to attach to a bug report. ## Audit [Section titled “Audit”](#audit) Checks every tour against the current page, sorted by severity. The tab shows a count of errors and warnings. * **Schema**: unknown values, misspelled fields, missing required fields and wrong types, each with the value that was probably meant. The same checks as [`validateTour`](/reference/schema/#checking-a-tour). * **Errors**: duplicate step ids, a tour with no steps, a step that advances on a click the tour blocks, an unregistered custom condition, and text contrast below 3:1 when a tour overrides colors. * **Warnings**: targets missing from this page, structural selectors likely to break, steps with no text, conditions pointing at unknown tours, contrast below the WCAG AA ratio, and beacons drawn on top of each other. * **Info**: long bodies, tours with no trigger, tours that show on every page load, a `none` beacon set to open on click (it always opens on hover), and a hover beacon with several steps (it stays open until closed). It also measures the page as it is now, which catches the problems a schema check cannot: a target that exists but has no visible box, one that is transparent, tiny, off screen, or covered by a fixed panel, and a click step whose target is disabled. ## Performance [Section titled “Performance”](#performance) Play a tour to record it. The tab shows the time to show each step, how often the popover moved, long frames and the worst frame while a step was visible, and how many DOM nodes the tour added. ## Options [Section titled “Options”](#options) ```ts const unmount = mount(docent, { open: true, // start open (otherwise remembered per tab) shortcut: { key: 'k', ctrlKey: true }, // or false to disable persist: false, // don't keep preferences or edits in localStorage document: iframe.contentDocument, // mount into another document }) ``` # Events and hooks > Send tour activity to your analytics, and run your own code before and after each step. Docent tells you what happens in two ways. **Events** are records you can send anywhere, such as analytics. **Hooks** are functions that run at a moment in the tour, so you can prepare the page or react to it. ## Events [Section titled “Events”](#events) Every lifecycle event goes to the `sink` you pass in. Connect it to your analytics to learn where people drop off. ```ts createTour(tour, { sink: { emit: (e) => analytics.track(e.type, { tour: e.tourId, step: e.stepId }), }, }) ``` | Event | When | | ---------------- | ------------------------------------------------------------------------------ | | `tour:started` | a tour begins | | `tour:completed` | the person reaches the end | | `tour:skipped` | they close or skip it | | `tour:aborted` | it ends for another reason, such as a missing target with `onMissing: 'abort'` | | `step:shown` | a step appears | | `step:completed` | a step is finished and the tour moves on | | `step:skipped` | a step is passed over, for example by a condition | | `step:missing` | a step’s target could not be found | | `beacon:shown` | a [beacon](/guides/beacons/) appears on the page for the first time | | `beacon:opened` | the reader opens a beacon; `via` is `click` or `hover` | Each event carries the tour id and version, the step id and index where it applies, a timestamp, and the user’s identity. Beacon events come from the manager, so they reach the `sink` given to `createDocent`. To send events to several places, combine sinks. `@docentjs/core` comes with every Docent package; add it to your own dependencies to import from it directly. ```ts import { combineSinks } from '@docentjs/core' createTour(tour, { sink: combineSinks(analyticsSink, loggingSink) }) ``` ## Hooks [Section titled “Hooks”](#hooks) Hooks are functions, so they live in your code rather than in the tour JSON. ```ts createTour(tour, { hooks: { onStart: (tour) => {}, onStepChange: ({ step, index }) => {}, onComplete: (tour) => {}, onSkip: ({ step }) => {}, onAbort: (tour, reason) => {}, steps: { 'menu-item': { beforeShow: async () => { await openMenu() // make sure the target exists }, afterShow: () => {}, beforeHide: () => closeMenu(), }, }, }, }) ``` `beforeShow` is the most useful one. It runs before the step looks for its target, so it can open a menu, expand a section, switch a tab or fetch data. Return `false` from it to skip the step. Tip With the manager, pass hooks keyed by tour id: `createDocent({ tours, hooks: { onboarding: { steps: { … } } } })`. ## Reading the state [Section titled “Reading the state”](#reading-the-state) Subscribe to a controller to follow the tour, or read the state on demand: ```ts const unsubscribe = controller.subscribe((state) => { console.log(state.status, state.index) }) controller.getState() // { status, index, history, reason? } ``` | Status | Meaning | | --------------------------------- | ---------------------------------------------- | | `idle` | not started | | `running` | a step is showing | | `paused` | waiting for the user to reach the step’s route | | `completed`, `skipped`, `aborted` | ended, and how | # Tour manager > Give Docent all your tours and let it decide which one to show, to whom, and when. `createTour` runs one tour when you tell it to. Real products have several tours: a welcome for new users, a walkthrough of a feature the first time someone opens it, an announcement for people on a certain plan. The **manager** holds all of them and applies each tour’s rules for you. ```ts import { createDocent } from '@docentjs/dom' import { invoices, welcome, whatsNew } from './tours' const docent = createDocent({ tours: [welcome, invoices, whatsNew] }) docent.identify(user.id, { plan: user.plan, role: user.role }) ``` That is the whole integration. The rules live in each tour’s JSON, not in your app code. ## What happens behind the scenes [Section titled “What happens behind the scenes”](#what-happens-behind-the-scenes) 1. The manager loads your tours, from an array or from a [`TourSource`](#loading-tours-from-an-api), and each user’s saved progress. 2. It watches each tour’s **trigger**: a route, an element appearing, an event your app reports, or the page loading. 3. When a trigger fires, it checks the tour’s **conditions** against the user’s traits and the page. 4. It checks the tour’s **frequency** against that user’s history. A tour set to show once will not show again after it was completed or skipped. 5. If everything passes and no other tour is running, the tour starts. If another tour is running, it waits in a queue and is tried again when that one ends. ## Writing the rules [Section titled “Writing the rules”](#writing-the-rules) ```ts defineTour({ id: 'invoices', trigger: { type: 'route', pattern: '/invoices/**', delay: 800 }, conditions: [{ type: 'trait', key: 'plan', op: 'eq', value: 'trial' }], options: { frequency: 'until-completed', persist: true }, steps: [ … ], }) ``` In words: when a trial user opens any invoices page, wait 800 ms, then show this tour. Keep offering it until they finish it, and let them pick up where they left off. | Trigger | The tour starts when | | ------------------------------------- | ------------------------------------------------ | | `{ type: 'auto', delay? }` | the page loads, at most once per page load | | `{ type: 'route', pattern, delay? }` | the user is on, or navigates to, a matching path | | `{ type: 'element', target, delay? }` | the element appears on the page | | `{ type: 'event', name }` | your code calls `docent.track(name)` | | `{ type: 'manual' }` or none | only when you call `docent.start(id)` | Every condition type and frequency option is in [Triggers and conditions](/guides/triggers-and-conditions/). ## Telling it who the user is [Section titled “Telling it who the user is”](#telling-it-who-the-user-is) ```ts docent.identify(user.id, { plan: 'trial', seats: 5, beta: true }) ``` Traits are what `trait` conditions compare against. Progress is stored per user id, so two people sharing a browser, or you switching between test accounts, each get their own history. Anonymous visitors share one unscoped history. Call `identify` again whenever the user or their traits change, for example after an upgrade. Triggers are checked again straight away. ## Reporting what users do [Section titled “Reporting what users do”](#reporting-what-users-do) ```ts docent.track('invoice-saved') ``` This starts tours whose trigger is `{ type: 'event', name: 'invoice-saved' }`, and moves on a running step that is waiting for `{ on: 'event', name: 'invoice-saved' }`. ## Starting a tour yourself [Section titled “Starting a tour yourself”](#starting-a-tour-yourself) ```ts docent.start('welcome') // for "Take the tour again" buttons docent.start('welcome', { at: 'billing' }) // from a specific step ``` A manual start ignores the trigger, conditions and frequency, and replaces any tour already running. The result, completed or skipped, is still recorded. ## Tours that follow each other [Section titled “Tours that follow each other”](#tours-that-follow-each-other) Triggers are checked again whenever a tour ends. A tour with this condition starts right after `welcome` is completed, if its own trigger still holds: ```ts conditions: [{ type: 'tour', id: 'welcome', state: 'completed' }] ``` ## Small screens [Section titled “Small screens”](#small-screens) A tour made for a wide layout can point at elements that are hidden on a phone. Set `minViewportWidth` to show nothing below a width, in px: ```ts createDocent({ tours, minViewportWidth: 768 }) // no tours on phones ``` Below that width no trigger fires and `docent.start()` does nothing. If the window later grows past it, triggers are checked again, so the tour can still start. A tour already running is not stopped when the window shrinks. To limit a single tour, put it in the tour’s options instead. When both are set, the larger width wins. ```ts options: { minViewportWidth: 1024 } ``` ## Client-side routers [Section titled “Client-side routers”](#client-side-routers) The manager notices navigation through `popstate`, `hashchange` and the Navigation API, which covers React Router, Vue Router, SvelteKit, Next.js and most others. If your router changes the URL without any of those, call `docent.refresh()` after each navigation. ## Loading tours from an API [Section titled “Loading tours from an API”](#loading-tours-from-an-api) `tours` also accepts a `TourSource`: an object with `load()`, and optionally `subscribe()` for live updates. Fetch tours from your backend, and changes reach users without a deploy. ```ts createDocent({ tours: { load: () => fetch('/api/tours').then((r) => r.json()), }, }) ``` ## Frameworks [Section titled “Frameworks”](#frameworks) In React, Vue and Svelte, use `useDocent` from the adapter instead of `createDocent`. It creates the manager for the component’s lifetime and lets you render your own popover. See [React](/frameworks/react/), [Vue](/frameworks/vue/) and [Svelte](/frameworks/svelte/). ## Reference [Section titled “Reference”](#reference) | Member | What it does | | ------------------------------ | -------------------------------------------------------------------- | | `ready` | a promise that resolves once tours and progress are loaded | | `identify(id, traits)` | set the current user | | `track(name)` | report an event | | `start(id, { at? })`, `stop()` | start a tour by hand, or end the running one (recorded as skipped) | | `reset(id?)` | forget progress, so a tour shows again | | `refresh()` | check triggers again, after a navigation the manager could not see | | `isEligible(id)` | whether conditions and frequency would allow the tour right now | | `tourState(id)` | `not-started`, `in-progress`, `completed` or `skipped` for this user | | `getState()`, `subscribe(fn)` | `{ active, tours }`, now or whenever it changes | | `updateTour(tour)` | replace a tour’s definition, even while it runs | | `activeController` | the running tour’s controller, for fine control | | `destroy()` | stop watching and remove everything | `createDocent` also takes `minViewportWidth`, `storage`, `sink`, `custom` condition predicates, `hooks` keyed by tour id, and `renderer` options that apply to every tour. Tip To see why a tour is or is not showing for the current user, open the [devtools](/guides/devtools/). Each tour gets a verdict and the exact reason, such as a failed condition with the user’s real value. # Routes and persistence > Tours that span several pages, and tours that pick up where the user left off after a reload. ## Multi-page tours [Section titled “Multi-page tours”](#multi-page-tours) Some tours need to cross pages: open the invoices list, then explain the editor. Give each step a `route` pattern saying which page it belongs to. ```ts steps: [ { id: 'open', route: '/app', target: { name: 'nav-invoices' }, advance: { on: 'click' } }, { id: 'new', route: '/app/invoices', target: { name: 'new-invoice' } }, ] ``` When the current path does not match the step’s route, the tour pauses and hides everything. When the user arrives on the right page, it carries on. Here, clicking the nav link navigates to `/app/invoices`, and the second step appears there. Patterns match the path only; query strings and hashes are ignored. | Pattern | Matches | | ------------ | ------------------------------- | | `/settings` | exactly `/settings` | | `/users/:id` | `/users/42`, one segment | | `/users/*` | any one segment after `/users/` | | `/docs/**` | `/docs` and anything below it | `createTour` notices navigation through `popstate`, `hashchange` and the Navigation API, which covers most client-side routers. If yours changes the URL without any of those, call `controller.routeChanged()` after each navigation. ## Remembering progress [Section titled “Remembering progress”](#remembering-progress) With `options.persist: true`, the current step is saved as the user moves through the tour. Call `resume()` instead of `start()` and the tour continues from that step after a reload or a full page navigation. ```ts const tour = createTour(onboarding) // options: { persist: true } tour.resume() ``` Progress is keyed by tour id and saved in `localStorage`, with an in-memory fallback when storage is blocked. The same record tracks whether the tour was completed or skipped, which is what [frequency](/guides/triggers-and-conditions/#frequency) rules read. ## Saving progress somewhere else [Section titled “Saving progress somewhere else”](#saving-progress-somewhere-else) To keep progress on your server, so it follows the user across devices, pass any object with `get`, `set` and `remove`. They may be synchronous or return promises. ```ts createTour(tour, { storage: { get: (key) => api.get(`/tour-progress/${key}`), set: (key, value) => api.put(`/tour-progress/${key}`, value), remove: (key) => api.delete(`/tour-progress/${key}`), }, }) ``` Note The manager scopes keys per user once you call `identify`, so one storage adapter serves every user. # Steps and advancing > What a step can say, where its popover sits, and the different ways it can move on to the next one. A step is one stop on the tour: an element, a short explanation, and a way to continue. Good steps say one thing. If a step needs three paragraphs, it is probably two steps. ## Content [Section titled “Content”](#content) ```ts { id: 'invoices', target: { name: 'invoices-tab' }, title: 'Invoices', body: 'Click **Invoices** to see everything you have billed.', format: 'markdown', media: { type: 'image', src: '/help/invoices.png', alt: 'The invoices list' }, } ``` * `title` is the heading. Keep it short, ideally under six words. * `eyebrow` is a small line above it. Set it once for the whole tour, or per step; `''` removes it for one step. See [themes](/customize/themes/). * `body` is plain text by default. * With `format: 'markdown'`, a safe subset is rendered: **bold**, *italic*, `code`, links, paragraphs and line breaks. * `media` adds an image or a video above the text. Always give images an `alt`. Note Raw HTML is never injected, even with markdown on. Tours loaded from a server or written in a builder are safe to render as they are. ## Placement [Section titled “Placement”](#placement) `placement` says where you would like the popover. It is a preference, not an order: ```ts placement: 'auto' | 'top' | 'right' | 'bottom' | 'left' | 'top-start' | 'top-end' | 'bottom-start' | 'bottom-end' | 'left-start' | 'left-end' | 'right-start' | 'right-end' ``` If the preferred side has no room, the popover flips to the opposite side, then tries the other two, and finally slides along the edge to stay on screen. `-start` and `-end` align the popover with one end of the element instead of its middle. ### Small screens [Section titled “Small screens”](#small-screens) Below 480 px wide there is no room beside an element, so the popover becomes a card as wide as the screen, with a margin around it. When the card fits above or below the element, it sits there and points at it; the step’s `placement` picks which side it tries first. When it fits on neither side, it docks near the bottom edge, still pointing at the element, and the page scrolls so the element stays visible above it. An element pinned to the bottom of the screen, such as a tab bar, or at the very end of the page gets the card docked at the top instead. Set `options.mobile.layout` to choose: `'auto'` (the default, as above), `'float'` to always sit beside the element, or `'dock'` to always dock: ```json "options": { "mobile": { "layout": "dock" } } ``` The card itself is laid out for thumbs on a small screen: progress becomes one segment per step across the top, filling as each step opens; the main button (Next or Done) is full width; Back and Skip sit beneath it as quiet text. A step with no target, such as a welcome or a finish, is centred on the screen with a larger title and its image at the top. Set `"mobile": { "card": "classic" }` to keep the large-screen card on phones. For a lighter touch, `"mobile": { "card": "compact" }` turns the card into a coachmark: sized by its content (up to about 70% of the screen), right beside the element and pointing at it, as on a large screen, so most of the page stays in view. Progress becomes a row of dots, Back a round arrow, Next a small pill, and Skip is left to the close button. The buttons are drawn small but keep a full-size area to tap. It never docks unless `layout` is `'dock'`: when neither side has room, it overlaps the edge of the element from above or below instead. ```json "options": { "mobile": { "card": "compact" } } ``` Change the width with the renderer’s `sheetBreakpoint`, or set it to `0` to lay the popover out as on a large screen at every size. The popover never grows taller than the screen. When a step has more text than fits, the text scrolls inside the card, the last line fades while more remains, and the buttons stay put at the bottom. On short screens, such as a phone held sideways, spacing and type tighten a little. If no side has room, the popover sits over part of the element rather than off the edge of the screen. ## Moving on [Section titled “Moving on”](#moving-on) By default the person presses Next. The `advance` field lets the tour follow what they actually do instead, which teaches far better than reading. | `advance` | The step moves on when | | ------------------------------------ | ----------------------------------------------------------------------------------------- | | `'button'` | the person presses Next (default) | | `{ on: 'click' }` | they click the target | | `{ on: 'input', match: '^\\S+@' }` | what they type matches the regular expression | | `{ on: 'event', name: 'saved' }` | your code calls `controller.notify('saved')`, or `docent.track('saved')` with the manager | | `{ on: 'element', target: '#menu' }` | an element appears | | `{ on: 'delay', ms: 4000 }` | the time passes | Normally the spotlighted element cannot be clicked, so nobody triggers an action by accident mid-tour. Click and input steps make it clickable automatically. For any other step, set `interaction: 'allow'` to let people use the element. LiveClick, type, and a blocked elementPlay Continueyou\@example.comDelete everything ## Buttons and wording [Section titled “Buttons and wording”](#buttons-and-wording) Hide buttons on one step: ```ts buttons: { back: false, skip: false, close: false, next: false } ``` Change the wording for a whole tour with `options.labels`, or for every tour at once with the renderer’s `labels`: ```ts options: { labels: { next: 'Continue', back: 'Previous', skip: 'Not now', done: 'Finish', progress: 'Step {current} of {total}' }, // `{current2}` and `{total2}` pad to two digits: `03 / 09`. } ``` ## Progress [Section titled “Progress”](#progress) With `showProgress` on, the popover shows how far along the person is. The count includes every step, even ones later skipped by a condition, so the numbers never jump backwards mid-tour. `progress` picks how it is drawn: `meter` (the default, a slim bar and the count), `count`, `ticks`, `dots`, or `none`. ```ts options: { showProgress: true, progress: 'dots' } ``` ## Skipping steps for some people [Section titled “Skipping steps for some people”](#skipping-steps-for-some-people) A step-level `condition` skips that step when it is false, for example a step about admin settings that only admins should see: ```ts { id: 'billing', condition: { type: 'trait', key: 'role', op: 'eq', value: 'admin' }, … } ``` Conditions are covered in [Triggers and conditions](/guides/triggers-and-conditions/). # Targets > Tell each step which element to spotlight, in a way that survives redesigns and elements that appear late. A step’s `target` says which element it points at. Tours break most often because an element was renamed, moved or not rendered yet. This page covers the ways to avoid that. ## Name your elements [Section titled “Name your elements”](#name-your-elements) Add a `data-docent` attribute to the element and refer to it by name. ```html ``` ```ts { id: 'save', target: { name: 'save' }, title: 'Save your work' } ``` This is the most reliable option. Class names change when styles change, and structure changes when layouts change, but an attribute that exists only for the tour stays put. It also tells anyone reading the markup that a tour depends on this element. ## CSS selectors [Section titled “CSS selectors”](#css-selectors) A plain string is a CSS selector. It is handy for elements you cannot add attributes to, such as third-party widgets. ```ts { id: 'save', target: '#save-button' } ``` ## Fallbacks [Section titled “Fallbacks”](#fallbacks) List several selectors and the first one that matches wins. This keeps a tour working while markup is in flux. ```ts { target: { name: 'save', selectors: ['[data-testid="save"]', 'form button[type=submit]'], }, } ``` The `name` is tried first, then each selector in order. An invalid selector is skipped rather than throwing. If nothing matches in the page, open shadow roots are searched as well. ## Narrowing the search [Section titled “Narrowing the search”](#narrowing-the-search) When a selector matches many elements, `within` limits the search to a container and `nth` picks one of the matches, counting from 0. ```ts { target: { selectors: ['.invoice-row'], within: '#invoices', nth: 2 } } ``` ## Elements that appear late [Section titled “Elements that appear late”](#elements-that-appear-late) Menus, modals and data-driven lists often render after the step starts. By default, a step whose target is missing is skipped. Ask it to wait instead: ```ts { id: 'menu-item', target: '#menu-item', onMissing: 'wait', waitFor: 5000 } ``` The renderer watches the page and shows the step the moment the element appears. If it has not appeared after `waitFor` milliseconds (3000 by default), the step is skipped. | `onMissing` | When the target is not there | | ----------- | -------------------------------------------------- | | `'skip'` | move on to the next step (default) | | `'wait'` | wait up to `waitFor` ms, then skip | | `'abort'` | end the tour, for steps the tour cannot do without | Try it: the first step asks you to click, and the second waits for an element that appears 600 ms later. LiveA step that waits for its elementPlay Reveal after 600 msLazy element Tip If an element only appears after the user does something, such as opening a menu, a `beforeShow` [hook](/guides/events-and-hooks/#hooks) can do that for them before the step looks for its target. ## Steps without a target [Section titled “Steps without a target”](#steps-without-a-target) Leave `target` out and the step shows as a centred card with the page dimmed behind it. Use it to welcome people at the start and to say what comes next at the end. ## Finding good targets [Section titled “Finding good targets”](#finding-good-targets) The [devtools](/guides/devtools/) have a target picker: click any element on the page and it suggests selectors ranked by how likely they are to survive changes, and warns about fragile ones such as generated class names. # Triggers and conditions > Decide when a tour starts, who sees it, how often, and which of its steps each person gets. Three fields in a tour’s JSON answer three questions: * **`trigger`**: when should the tour start? * **`conditions`**: who should see it? * **`options.frequency`**: how often? The [tour manager](/guides/manager/) applies them. A single tour started with `createTour(tour).start()` ignores all three, because you already decided to show it. ## Triggers [Section titled “Triggers”](#triggers) ```ts trigger: { type: 'auto', delay: 1000 } // when the page loads trigger: { type: 'route', pattern: '/invoices/**' } // on a matching path trigger: { type: 'element', target: '#new-menu' } // when an element appears trigger: { type: 'event', name: 'invoice-saved' } // when you call docent.track('invoice-saved') trigger: { type: 'beacon' } // when the reader opens a beacon trigger: { type: 'manual' } // only docent.start(id) ``` `delay` waits before starting, so the page can settle. When the delay ends, the trigger is checked again: a route tour does not start if the user has already moved to another page. Route patterns are explained in [Routes and persistence](/guides/routes-and-persistence/#multi-page-tours). A `beacon` trigger is different from the rest: instead of starting the tour, it puts a small mark on the page and waits for the reader to open it. [Beacons](/guides/beacons/) has the details. ## Conditions [Section titled “Conditions”](#conditions) Conditions are data too, so they live in the tour JSON and can be evaluated anywhere. All of a tour’s `conditions` must hold for it to show. ```ts conditions: [ { type: 'trait', key: 'plan', op: 'eq', value: 'trial' }, { type: 'trait', key: 'role', op: 'in', value: ['owner', 'admin'] }, { type: 'not', condition: { type: 'tour', id: 'welcome', state: 'completed' } }, ] ``` This reads: trial users who are owners or admins and have not completed the welcome tour. | Condition | True when | | ------------------------------------------------------------ | ------------------------------------------------------------------------------------ | | `{ type: 'trait', key, op, value }` | the user’s trait compares true; see the operators below | | `{ type: 'route', pattern }` | the current path matches | | `{ type: 'element', target, exists? }` | the element is on the page, or with `exists: false`, is not | | `{ type: 'tour', id, state }` | another tour is `not-started`, `in-progress`, `completed` or `skipped` for this user | | `{ type: 'all', conditions }`, `{ type: 'any', conditions }` | every one, or at least one, holds | | `{ type: 'not', condition }` | the inner condition does not hold | | `{ type: 'custom', name, args? }` | your own predicate, registered by name | Traits come from `docent.identify(id, traits)`. The operators are: | Operator | Meaning | | ------------------------ | ----------------------------------------- | | `eq`, `neq` | equal, not equal | | `gt`, `gte`, `lt`, `lte` | greater or less than, for numbers | | `in`, `nin` | the trait is, or is not, one of a list | | `contains` | a list or string trait contains the value | | `exists`, `missing` | the trait is set, or not set | ### Your own conditions [Section titled “Your own conditions”](#your-own-conditions) For anything the built-in types cannot express, register a predicate by name and refer to it from the JSON: ```ts createDocent({ tours, custom: { isWeekend: () => [0, 6].includes(new Date().getDay()) }, }) // in the tour: conditions: [{ type: 'custom', name: 'isWeekend' }] ``` ### Conditions on a single step [Section titled “Conditions on a single step”](#conditions-on-a-single-step) A step’s `condition` skips just that step when it is false. This works with `createTour` too. ```ts { id: 'team', condition: { type: 'trait', key: 'seats', op: 'gt', value: 1 }, title: 'Invite your team' } ``` ## Frequency [Section titled “Frequency”](#frequency) | `options.frequency` | Shows | | ------------------- | ----------------------------------------------------- | | `'once'` (default) | once per version, whether it was completed or skipped | | `'until-completed'` | again after a skip, until it is completed | | `'always'` | every time the trigger fires | An `auto` trigger fires at most once per page load, even with `always`. Showing an updated tour again Bump the tour’s `version` after a meaningful change. Everyone who saw an older version will see it once more. # React > Hooks, a component and a provider for React 18 and 19, including your own popover component. [Ledgerline — the React example](/examples/react/)A receivables console with four tours: an auto-started walkthrough, a hint, a release note and a popover drawn by React. ```sh pnpm add @docentjs/react ``` This is the only package a React app needs. It re-exports `defineTour`, the types such as `RenderContext`, and `createLocalStorage`. Theme presets come from `@docentjs/react/themes`. Note The tour definition type is exported as `TourDefinition`, because `Tour` is the name of the component. ## One tour: `useTour` [Section titled “One tour: useTour”](#one-tour-usetour) ```tsx import { useTour } from '@docentjs/react' import { welcomeTour } from './tours' function Dashboard() { const tour = useTour(welcomeTour) return ( <>

Status: {tour.state.status}

) } ``` The hook returns the state and the controls: `start`, `resume`, `next`, `back`, `skip`, `goTo` and `notify`. The tour is created once and cleaned up when the component unmounts. It is recreated only when the tour’s `id` or `version` changes. Define tours outside your components, or memoize them, so they are not rebuilt on every render. ## Your own popover [Section titled “Your own popover”](#your-own-popover) Pass a render function as `popover`. Your component renders inside the box Docent positions, through a portal, so context, hooks and your CSS all work as usual. Render `tour.portal` once, anywhere in your tree. ```tsx import type { RenderContext } from '@docentjs/react' function Card({ ctx }: { ctx: RenderContext }) { return (

{ctx.step.title}

{ctx.step.body}

) } function Dashboard() { const tour = useTour(welcomeTour, { popover: (ctx) => }) return ( <> {tour.portal} ) } ``` Docent still draws the spotlight, positions your card, and handles the keyboard and focus. See [Headless mode](/customize/headless/) for what the context contains. ## The `` component [Section titled “The \ component”](#the-tour-component) The component form renders the portal for you and passes the controls to its children. `autoStart` starts the tour on mount; `autoStart="resume"` continues saved progress. ```tsx }> {(t) => } ``` ## Shared settings: `` [Section titled “Shared settings: \”](#shared-settings-docentprovider) Set the theme, templates, user, storage and analytics once for every tour below the provider. ```tsx import { DocentProvider } from '@docentjs/react' import { minimal } from '@docentjs/react/themes' analytics.track(e.type, e) }} > ``` ## Many tours: `useDocent` [Section titled “Many tours: useDocent”](#many-tours-usedocent) `useDocent` creates the [tour manager](/guides/manager/) for the component’s lifetime. Use it once, near the root of your app. ```tsx import { useDocent } from '@docentjs/react' import { invoices, welcome } from './tours' function App({ user }) { const docent = useDocent({ tours: [welcome, invoices], popover: (ctx) => }) useEffect(() => { if (user) docent.identify(user.id, { plan: user.plan }) }, [user]) return <>{docent.portal} } ``` Tours now start from their own triggers, conditions and frequency. `docent.state.active` is the id of the running tour, and `docent.start(id)` starts one by hand. It works under React StrictMode. ## Devtools [Section titled “Devtools”](#devtools) ```tsx import { DocentDevtools } from '@docentjs/devtools/react' ``` It renders nothing in production builds. See [Devtools](/guides/devtools/). # Svelte > Stores, a component prop and an action for Svelte 4 and 5, including your own popover component. [Pressroom — the Svelte example](/examples/svelte/)A newsroom desk with three tours, including replaced footer buttons and a popover component passed to useTour. ```sh pnpm add @docentjs/svelte ``` This is the only package a Svelte app needs. It re-exports `defineTour`, the types such as `RenderContext`, and `createLocalStorage`. Theme presets come from `@docentjs/svelte/themes`. ## One tour: `useTour` [Section titled “One tour: useTour”](#one-tour-usetour) `useTour` returns readable stores for the state, plus the controls: `start`, `resume`, `next`, `back`, `skip`, `goTo` and `notify`. ```svelte

Status: {$state.status}

``` Call `destroy` when the component goes away, as above. ## Your own popover [Section titled “Your own popover”](#your-own-popover) Pass a component as `popover`. Docent mounts it into the box it positions for every step, with `ctx` as a prop, and unmounts it before the next one. ```svelte ``` Card.svelte ```svelte

{ctx.step.title}

{ctx.step.body}

``` Docent still draws the spotlight, positions your card, and handles the keyboard and focus. See [Headless mode](/customize/headless/) for what the context contains. ## The `tour` action [Section titled “The tour action”](#the-tour-action) `use:tour` starts a tour when an element mounts and cleans it up when the element is removed. It suits tours tied to one part of the page. ```svelte
…
``` ## Many tours: `useDocent` [Section titled “Many tours: useDocent”](#many-tours-usedocent) `useDocent` creates the [tour manager](/guides/manager/). Use it once, near the root of your app. ```svelte ``` Leave out `popover` to use the built-in popover. ## Devtools [Section titled “Devtools”](#devtools) ```svelte ``` It renders nothing in production builds. See [Devtools](/guides/devtools/). # Vue > Composables and a teleporting component for Vue 3, including your own popover component. [Cinder — the Vue example](/examples/vue/)An incident console with three tours, including one triggered by an element appearing and a popover teleported through . ```sh pnpm add @docentjs/vue ``` This is the only package a Vue app needs. It re-exports `defineTour`, the types such as `RenderContext`, and `createLocalStorage`. Theme presets come from `@docentjs/vue/themes`. ## One tour: `useTour` [Section titled “One tour: useTour”](#one-tour-usetour) ```vue ``` `state` and `active` are refs. The composable also returns the controls: `start`, `resume`, `next`, `back`, `skip`, `goTo` and `notify`. Called inside `setup()`, the tour is cleaned up with the component. Outside a component, call `tour.destroy()` yourself. ## Your own popover [Section titled “Your own popover”](#your-own-popover) Turn on headless mode with `popover: true` and place a `` anywhere in the template. Its default slot is teleported into the box Docent positions, and receives the render context. ```vue ``` Docent still draws the spotlight, positions your card, and handles the keyboard and focus. See [Headless mode](/customize/headless/) for what the context contains. ## Shared settings [Section titled “Shared settings”](#shared-settings) Call `provideDocentDefaults` in a parent component’s `setup()` to set the theme, templates, user, storage and analytics for every tour below it. ```ts import { provideDocentDefaults } from '@docentjs/vue' import { minimal } from '@docentjs/vue/themes' provideDocentDefaults({ renderer: { theme: minimal, templates: { card } }, identity: { id: user.id, traits: { plan: user.plan } }, }) ``` ## Many tours: `useDocent` [Section titled “Many tours: useDocent”](#many-tours-usedocent) `useDocent` creates the [tour manager](/guides/manager/). Use it once, near the root of your app. ```vue ``` Leave out `popover: true` and `` to use the built-in popover. ## Devtools [Section titled “Devtools”](#devtools) ```vue ``` It renders nothing in production builds. See [Devtools](/guides/devtools/).