Tour
A guided sequence of Spotlights: point at one thing, say something, and move to the next with Back / Next / Skip and a progress readout.
Live demo
Tour is a Spotlight with more than one step. It owns the sequence — which step is showing, the Back / Next / Skip / Done buttons, and the progress readout — and drives a single composed <xtyle-spotlight> through it, so every step gets the same honest isolation: the page dims, a hole is cut over the target, and a callout points at it, with the target left live underneath.
Each step is an <xtyle-tour-step> carrying a target and, as its content, whatever the callout should say; any spotlight knob (heading, placement, shape, pulse, arrow, dim, blur, no-dismiss) set on a step overrides the Tour's default for that step alone. The Tour resolves each step's target against the page and hands the element to the spotlight directly, so a selector still finds a node the tour's own shadow root can't see, and the step-to-step focus handling — the part that goes wrong when a sequence is hand-rolled — is the spotlight's, already proven. The only chrome the Tour invents is the nav row; it renders through component.tour, so a mod can reshape Back / Next / Skip and the progress dots without touching the sequencing.
When to use
How this component composes with the rest of the set.
Props
21 props, straight from the manifest.
| Prop | Type | Default | Bindings | Description |
|---|---|---|---|---|
start(), next(), back(), go(), finish(), skip(), and close() are the imperative doors. | ||||
count prints "2 of 5", dots draws one dot per step, none shows nothing. | ||||
{ id, title, summary, steps } - in place of authoring TourStep children by hand. It materializes those children rather than rendering a second way, so a spec-driven tour behaves identically to a slotted one; the same items-mode/slotted-mode split tabs and accordion carry. Declaring a tour as data is what lets it be held in a variable, addressed by id from a help menu or a command palette, listed in a picker, or contributed by a mod - none of which markup living inside one screen can be. Set it as a property from JS, or as a JSON string in markup, the way Progress takes a ramp stop list. | ||||
step fires on arrival, which is already too late. Property only; it is a function. | ||||
target-missing says it never did. | ||||
complete and skip separately and keeps no memory of either: which storage holds that, under what key, whether it is per-user or per-device, and whether it resets when the tour's content changes are all the app's decisions. So this is the app telling the component, not the component guessing. | ||||
noDismiss so the veil and Escape can't leave it either. | ||||
bounce animates toward it, static holds still, none draws nothing. Overridable per step. | ||||
Events
What the component emits, and what rides along on event.detail. The name in the
first column is the one addEventListener takes.
| Event | Detail | Bindings | Description |
|---|---|---|---|
complete or skip. | |||
target selector never resolved, even after targetTimeout. The callout still opened, anchored to nothing - without this the step degrades in silence and reads exactly like a mistyped selector. |
Methods
What you can call on the element itself — the half of the surface a prop or an event cannot express. A control driven by a gesture, or one whose state the platform owns, opens here. Astro renders on the server and hands back no instance, so no method is reachable from it.
| Method | Returns | Bindings | Description |
|---|---|---|---|
start, and runs beforeStep before the step's target is measured. | |||
complete, which is the half of the take-versus-replay distinction a taken store watches. | |||
skip, so a store can tell the user who bailed from the user who finished. | |||
Appearance
States
running
A step is showing: the spotlight is open on the step's target and the nav is live.
Anatomy
The named parts that make up the component. A ::part() handle is reachable from your own stylesheet; the class beside it is the component's internal selector, which a shadow boundary keeps to itself.
tour
The controller. It paints nothing itself; it drives the composed spotlight and hosts the nav.
spotlight
The composed <xtyle-spotlight> the tour re-points at each step — the veil, the ring, and the callout are all its.
nav
The step controls in the callout's action row: Back, the progress readout, Skip, and Next / Done.
back
Steps back a step. Hidden on the first step.
progress
Which step you're on: 2 of 5 under count, a dot per step under dots, nothing under none.
skip
Ends the tour early. Hidden on the last step, where Done says the same thing.
next
Advances a step, and becomes Done on the last one.
Tokens & coverage
What the component consumes, checked live against what the algorithm produces.
Live coverage check against the xtyle-default register
(derive(xtyleDefault, { anchors }) →
coverComponent(manifest, register)). Every token this component
consumes must be a key the algorithm produces.
--accent--accent-fg--border-thin--duration-fast--ease-standard--fg-1--fg-2--line-2--radius-full--radius-sm--ring--space-1--space-2--space-3--state-hover--state-press--text-xs--weight-semiboldSlots
The steps, as <xtyle-tour-step> children. Each carries a target and holds the callout content for that step.
Accessibility
Code
A tour declared as data
spec takes the whole walkthrough as { id, title, summary, steps } instead of TourStep children, and materializes the same steps — so a spec-driven tour behaves identically to a slotted one. That is what lets a tour be held in a variable, addressed by id from a help menu or a command palette, listed in a picker, or contributed by a mod. Keeping it inert data is also why there is no registry here: a table of your tours is a plain object, and it stays yours. taken is the other half — the component reports complete and skip and remembers neither, so where that is stored is your call.
<xtyle-tour id="first-run" spec='{
"id": "first-run",
"title": "Getting around",
"steps": [
{ "target": "#rail", "heading": "The rail", "body": "Everything hangs off this." },
{ "target": "#canvas", "heading": "The canvas", "placement": "left" },
{ "target": "#help", "heading": "Help", "body": "And the tours live here." }
]
}'></xtyle-tour><script lang="ts">
import { Tour } from "@xtyle/svelte";
import type { TourSpec } from "@xtyle/core";
// A tour is data, so it can live anywhere and be addressed by id.
const TOURS = {
"first-run": {
id: "first-run",
title: "Getting around",
summary: "Three stops",
steps: [
{ target: "#rail", heading: "The rail", body: "Everything hangs off this." },
{ target: "#canvas", heading: "The canvas", placement: "left" },
{ target: "#help", heading: "Help", body: "And the tours live here." },
],
},
} satisfies Record<string, TourSpec>;
let taken = $state(localStorage.getItem("tour:first-run") === "done");
</script>
<Tour
spec={TOURS["first-run"]}
{taken}
oncomplete={() => { taken = true; localStorage.setItem("tour:first-run", "done"); }}
onskip={() => { taken = true; }}
/>---
import Tour from "@xtyle/astro/Tour.astro";
import type { TourSpec } from "@xtyle/core";
const firstRun: TourSpec = {
id: "first-run",
title: "Getting around",
steps: [
{ target: "#rail", heading: "The rail", body: "Everything hangs off this." },
{ target: "#canvas", heading: "The canvas", placement: "left" },
],
};
---
<Tour spec={firstRun} />A three-step onboarding tour
Point at the compose button, the inbox, and settings in turn, with a dot per step.
<xtyle-tour id="onboard" progress="dots">
<xtyle-tour-step target="#compose" heading="Start here" placement="bottom">
This is where a new message begins.
</xtyle-tour-step>
<xtyle-tour-step target="#inbox" heading="Everything lands here" placement="right">
Your inbox. Unread threads rise to the top.
</xtyle-tour-step>
<xtyle-tour-step target="#settings" heading="Make it yours" placement="left" shape="circle">
Theme, shortcuts, and accounts live in settings.
</xtyle-tour-step>
</xtyle-tour>
<script>
document.getElementById("onboard").start();
</script><script lang="ts">
import { Tour, TourStep, Button } from "@xtyle/svelte";
let touring = $state(false);
</script>
<Button onclick={() => (touring = true)}>Take the tour</Button>
<Tour bind:open={touring} progress="count" oncomplete={() => (touring = false)}>
<TourStep target="#compose" heading="Start here">Where a new message begins.</TourStep>
<TourStep target="#inbox" heading="Everything lands here" placement="right">Your inbox.</TourStep>
<TourStep target="#settings" heading="Make it yours" placement="left">Theme and accounts.</TourStep>
</Tour>---
import { Tour, TourStep } from "@xtyle/astro";
---
<Tour id="onboard" progress="dots">
<TourStep target="#compose" heading="Start here">Where a new message begins.</TourStep>
<TourStep target="#inbox" heading="Everything lands here" placement="right">Your inbox.</TourStep>
<TourStep target="#settings" heading="Make it yours" placement="left" shape="circle">Settings.</TourStep>
</Tour>
<script>
const tour = document.getElementById("onboard") as HTMLElement & { start(): void };
if (!localStorage.getItem("toured")) tour.start();
tour.addEventListener("close", () => localStorage.setItem("toured", "1"));
</script>