Guided onboarding walk: a dimmed stage, an orange ring around each target and a step card with Back, Skip and Next. Start it from a button; it ends itself. Reach for it on first use of a screen, and keep it to a handful of steps; use sui-stepper for a process the user works through.
Usage
<sui-tour></sui-tour>
<script type="module">
import { defineCustomElements } from "@shineui/core/loader";
import "@shineui/core/shineui.css";
defineCustomElements();
const el = document.querySelector("sui-tour");
el.steps = [
{ heading: "Bank feed lands here", body: "Overnight transactions wait for review.", target: "#tour-basic-feed" },
{ heading: "Watch the aging", body: "Overdue balances roll up here.", target: "#tour-basic-aging" },
{ heading: "One accent action", body: "The orange button is the main move.", target: "#tour-basic-post" },
]; /* SuiTourStep[] */
el.addEventListener("suiClose", (e) => console.log(e.detail));
</script>
import { SuiTour } from "@shineui/react";
import "@shineui/core/shineui.css";
export function Example() {
const steps = [
{ heading: "Bank feed lands here", body: "Overnight transactions wait for review.", target: "#tour-basic-feed" },
{ heading: "Watch the aging", body: "Overdue balances roll up here.", target: "#tour-basic-aging" },
{ heading: "One accent action", body: "The orange button is the main move.", target: "#tour-basic-post" },
]; /* SuiTourStep[] */
return <SuiTour steps={steps} onSuiClose={(e) => console.log(e.detail)} />;
}
<script setup>
import { SuiTour } from "@shineui/vue";
import "@shineui/core/shineui.css";
const steps = [
{ heading: "Bank feed lands here", body: "Overnight transactions wait for review.", target: "#tour-basic-feed" },
{ heading: "Watch the aging", body: "Overdue balances roll up here.", target: "#tour-basic-aging" },
{ heading: "One accent action", body: "The orange button is the main move.", target: "#tour-basic-post" },
]; // SuiTourStep[]
const onChange = (e) => console.log(e.detail);
</script>
<template>
<SuiTour :steps="steps" @sui-close="onChange" />
</template>
import { Component } from "@angular/core";
import { SuiTour } from "@shineui/angular";
@Component({
standalone: true,
imports: [SuiTour],
template: `<sui-tour [(open)]="open" [steps]="steps" (suiClose)="onChange($event)"></sui-tour>`,
})
export class Example {
open = false;
steps = [
{ heading: "Bank feed lands here", body: "Overnight transactions wait for review.", target: "#tour-basic-feed" },
{ heading: "Watch the aging", body: "Overdue balances roll up here.", target: "#tour-basic-aging" },
{ heading: "One accent action", body: "The orange button is the main move.", target: "#tour-basic-post" },
]; // SuiTourStep[]
onChange(e: CustomEvent) { console.log(e.detail); }
}
Basic
The card is a modal dialog labeled Product tour; focus is trapped on it and returns to it on every step, so the new heading and body are read. The stage takes every pointer event, including over the ring, so a step cannot ask the user to click the thing it points at.
Each step names a CSS selector; the tour measures the live element, scrolls it into view on arrival and re-measures on every scroll and resize, so the ring stays on it. The card sits below the ring and flips above it when there is no room.
// setup(root): properties and events
setup(root) {
const tour = root.querySelector('#tour-scroll');
tour.steps = [
{ heading: 'The oldest balance first', body: 'The ledger opens on the invoice that has waited longest.', target: '#tour-scroll-first' },
{ heading: 'A row below the fold', body: 'This step scrolls the panel to reach its target.', target: '#tour-scroll-late' },
{ heading: 'The whole ledger', body: 'A step can ring a container as well as a control.', target: '#tour-scroll-box' },
];
root.querySelector('#tour-scroll-start').addEventListener('click', () => {
tour.step = 0;
tour.open = true;
});
}
Centered welcome and wrap-up steps
A step with no target centers its card, which is what a welcome or a wrap-up wants. Back is disabled on the first step and Next reads Finish on the last.
// setup(root): properties and events
setup(root) {
const tour = root.querySelector('#tour-centered');
tour.steps = [
{ heading: 'Welcome to the ledger', body: 'Four screens run the month end. Here are two of them.' },
...walkthrough('tour-centered').slice(0, 2),
{ heading: 'That is the walk', body: 'Reopen it from Help whenever you need it.' },
];
root.querySelector('#tour-centered-start').addEventListener('click', () => {
tour.step = 0;
tour.open = true;
});
}
A fixed spotlight rectangle
Steps that name no selector fall back to the tour target rectangle, which is measured against the viewport rather than the page, so it suits chrome that is itself fixed, such as a top bar. Name a selector per step for anything that sits in the page.
<sui-button id="tour-rect-start" variant="outline" label="Spotlight the top bar"></sui-button>
<sui-tour id="tour-rect"></sui-tour>
// setup(root): properties and events
setup(root) {
const tour = root.querySelector('#tour-rect');
tour.steps = [{ heading: 'The workspace switcher', body: 'Every company you keep books for lives up here.' }];
tour.target = { top: '84px', left: '24px', width: '240px', height: '52px' };
root.querySelector('#tour-rect-start').addEventListener('click', () => {
tour.step = 0;
tour.open = true;
});
}
Step changes and close reasons
suiStepChange fires after Next or Back and carries the new index, and suiClose fires before the tour ends and says why: pick for Finish, cancel for Skip, close-button for the X, escape for Escape. Writing step or open from outside moves the tour and emits neither.
// setup(root): properties and events
setup(root) {
const tour = root.querySelector('#tour-veto');
const log = root.querySelector('#tour-veto-log');
tour.steps = walkthrough('tour-veto');
root.querySelector('#tour-veto-start').addEventListener('click', () => {
tour.step = 0;
tour.open = true;
});
tour.addEventListener('suiClose', (e) => {
if (e.detail === 'cancel') {
e.preventDefault();
log.textContent = 'Skip refused. Payroll setup has to be finished.';
return;
}
log.textContent = 'Ended by ' + e.detail + '.';
});
}
Keyboard
Key
Action
TabShift+Tab
Cycles through the X, Back, Skip and Next on the card and never leaves it.
EnterSpace
Presses the focused button. Next moves a step and returns focus to the card; Finish ends the tour with reason pick.
Escape
Ends the tour with reason escape, unless the listener refuses. Only the topmost open modal answers it.
API
<sui-tour>
Properties
Property
Attribute
Type
Default
Description
open
open
boolean
false
Shows the tour. Mutable; the element closes itself on Skip, Finish, the X or Escape unless the suiClose listener calls preventDefault(). Not in the reference, which mounts and unmounts the tour; added so the element follows every other overlay here. The element never opens itself, so there is no suiOpen; watch open. The stage paints in the browser's top layer while it shows, so a transformed, contained or clipping ancestor cannot move the ring off its target or cut the card off; the rest of the page is inert and does not scroll until the tour closes, and a node the page adds while it shows goes inert too. The card is role="dialog" with no aria-modal, so VoiceOver keeps reading the toast region; Tab goes on from the card into the toast stack and back. An element the page marks data-sui-keep-live stays usable with everything in it, and focus may move into it and back.
step
step
number
0
Current step index (the reference defaultStep). Mutable; Next and Back update it, and a value outside the steps is clamped to the nearest end, so a stale index shows the last step rather than nothing.
steps
property only
SuiTourStep[]
[]
Steps: { heading, body, target? }[]. Property, not attribute. A step with a target selector is measured live and followed on scroll and resize; a target outside the viewport is scrolled into view first, smoothly unless the reader asked for less motion, and the stage shows once the page has settled. The card sits below the ring and flips above it when there is no room, and it glides between two targets that are both in view. A step with no selector, and no whole-tour target, centers its card.
target
property only
SuiTourTargetSuiTourTarget | undefined
Fallback spotlight rect { top, left, width, height } as CSS lengths, for every step that names no target selector of its own. Property, not attribute. Omit to center the card.
Fires before the tour ends itself. Detail is why: 'pick' for Finish (the tour was completed), 'cancel' for Skip, 'close-button' for the X, 'escape' for Escape. Cancelable: preventDefault() keeps the tour open. It does not bubble.
suiStepChange
number
onSuiStepChange
@sui-step-change
Fires after Next or Back; detail is the new step index. Does not bubble.
Types
SuiCloseReasontypeWhy a dismissible surface is closing. Detail of the cancelable suiClose. A consumer expresses "a stray backdrop click must not close this" by calling preventDefault() on the reason it does not accept, which is why none of these needs a prop of its own.
No members.
SuiTourStepinterfaceOne tour step. heading is the reference title (renamed per the port conventions). target is a CSS selector for the element this step spotlights; the rectangle is measured from the live element and followed on scroll and resize, so the host does not measure anything.
heading
string
body
string
target?
string
SuiTourTargetinterfaceSpotlight rectangle as CSS lengths, relative to the viewport.
top?
string
left?
string
width?
string
height?
string
Theming
Every design token this element can render with, read from its own stylesheets and the ones it imports. Set a token on :root or on any ancestor and every use below follows it. The values shown are the ones in the theme you are viewing. The list is read per stylesheet, so an element that imports a shared drawing module lists that module's tokens too, whether or not it paints with all of them.