Skip to content

Getting started

This page starts from an empty Vite project and ends with a three-step tour on its page. The middle step lights a Save button and moves on only when the app reports that the save finished, which is the thing Leko is for.

You need Node 24.

Terminal window
npm create vite@latest my-tour -- --template vanilla-ts --no-interactive
cd my-tour
npm install

If npm asks to install create-vite, answer y.

Terminal window
npm install @annetaan/leko
npm install -D @annetaan/leko-codegen
src/tour.ts
import { createLeko } from '@annetaan/leko'
export const leko = createLeko()

There is one instance, exported, and every file that imports leko shares it. Its options are in createLeko and options.

src/stories.ts
import type { LekoStory } from '@annetaan/leko'
export const welcome = {
id: 'welcome',
steps: [
{ id: 'name', target: '#name', message: 'Your name goes here.' },
{
id: 'save',
target: { elements: '#save', interactive: true },
message: 'Press Save. This step moves on when the save has finished.',
awaits: 'profile-saved',
},
{ id: 'saved', target: '#status', message: 'Saved. The app said so, and the tour moved on.' },
],
} satisfies LekoStory

satisfies LekoStory checks the story without widening it, so awaits keeps its literal name and a mistyped one is reported on its own line, once the names are types (LekoKnownSignal).

The story has a file of its own for the reason One instance, in a module gives, and so that the mistyped name in the last section lands in a file that holds only the story.

The second step has no Next control, because it declares awaits: the only way past it is the save. interactive: true opens its hole, so the Save button under it is the real one and takes the click.

Replace the template’s src/main.ts with this:

src/main.ts
import { welcome } from './stories'
import { leko } from './tour'
document.querySelector<HTMLDivElement>('#app')!.innerHTML = `
<h1>Profile</h1>
<input id="name" value="Ada" />
<button id="save" type="button">Save</button>
<p id="status"></p>
`
const status = document.querySelector<HTMLParagraphElement>('#status')!
async function save() {
status.textContent = 'Saving…'
await new Promise((resolve) => setTimeout(resolve, 500))
status.textContent = 'Saved'
leko.reached('profile-saved')
}
document.querySelector<HTMLButtonElement>('#save')!.addEventListener('click', save)
leko.start(welcome)

The click listener is the app’s own save handler, and it would be there without a tour. The one line Leko adds is reached(), after the await, where the work has finished. It names what happened, not a story or a step, so it reads the same whether a tour is running or not. More on where it goes is under Where reached() goes.

Terminal window
npm run dev

Open the address Vite prints. The page dims, with a hole around the name field and a message beside it with a Next control. Next moves the hole to Save, and this message has no Next. Press Save: the page says “Saving…”, and half a second later “Saved”, and the tour moves to that text. Its Next ends the tour.

The “End tour” control in a corner of the page ends it at any step.

vite.config.ts
import { lekoSignals } from '@annetaan/leko-codegen/vite'
import { defineConfig } from 'vite'
export default defineConfig({
plugins: [lekoSignals({ out: 'src/leko-signals.d.ts' })],
})

A dev server that is already running does not pick up a new config file, so stop it with Ctrl+C and run npm run dev again.

The plugin writes src/leko-signals.d.ts, holding every name the app passes to reached(). Trimmed, it reads:

src/leko-signals.d.ts
declare module '@annetaan/leko' {
interface LekoSignals {
'profile-saved': true
}
// …
}

Now awaits takes only a name the app reports. Change it in src/stories.ts to 'profile-svaed' and run the compiler:

Terminal window
npx tsc
error TS2820: Type '"profile-svaed"' is not assignable to type '"profile-saved" | LekoUrlAwait | undefined'. Did you mean '"profile-saved"'?

That error is on the awaits line of src/stories.ts. The compiler also reports a longer one in src/main.ts, where the story is passed to start(), and it goes away with the first.

The editor shows the same error on that line, and offers 'profile-saved' as a completion. Put the name back. npm run build runs tsc first, so a mistyped name fails the build too.

Commit the generated file, for the reasons under Gathering the names.

  • React, Vue and a plain script says where each call goes in an app built with one.
  • Signals and awaits is why the save step has no Next, and what else decides when a signal counts.
  • Holes that are shown and holes that are open is why the name field’s hole does not take your typing and the Save button’s does.
  • Targets is how a step names what it points at, and what happens when that is hidden, late or off screen.
  • Several short stories is how one story leads to the next, and why a tour has no back control.
  • Across a page load is how a tour goes on after a link opens a new page.
  • Setup and clear-up is how a story and a step get the page ready and put it back, and how a tour waits for a load.
  • Styling is how the tour takes on your app’s look, and what options set instead of CSS.
  • The lifecycle is what happens between start() and the end of a tour.
  • Leko and Story and step cover every method and every field.
  • CSS custom properties covers the look, and import '@annetaan/leko/leko.css', the optional stylesheet that declares their defaults.
  • The Playground runs a tour over a real page.
  • The Examples run the sandbox’s cases on this site, each beside its story.
  • The sandbox cases each prove one behaviour, and show the code that does it.