Components / Overlays & Dialogs / Tour
Tour beta
A short guided walk: a ring on the thing, a card that explains it, and a way out at every step.
Share when you are ready
Projects start private. Invite your team from here when the draft is worth showing — you can change who sees it at any time.
Uses: Button — include its CSS alongside this one.
<div class="k-tour-stage" style="min-height:300px">
<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-pri btn-sm" type="button">New project</button>
<span class="k-tour-anchor"><button class="btn btn-sec btn-sm" type="button">Share</button><span class="k-tour-ring"></span></span>
<button class="btn btn-ghost btn-sm" type="button">Export</button>
</div>
<div class="k-tour" style="left:18px;top:78px" role="dialog" aria-label="Product tour">
<span class="k-tour-arrow" style="left:118px"></span>
<h4>Share when you are ready</h4>
<p>Projects start private. Invite your team from here when the draft is worth showing — you can change who sees it at any time.</p>
<div class="k-tour-ft"><span class="k-tour-dots"><i></i><i class="on"></i><i></i></span><button type="button" class="btn btn-ghost btn-sm">Skip</button><button type="button" class="btn btn-pri btn-sm">Next</button></div>
</div>
</div>/* KATA Tour — a ring on the target, a card beside it, an exit on every step */
.k-tour-stage{position:relative;border:1px solid var(--k-line);border-radius:14px;background:var(--k-bg-soft);padding:18px;min-height:196px;overflow:hidden}
.k-tour-anchor{position:relative;display:inline-flex}
.k-tour-ring{position:absolute;border:2px solid var(--k-accent);border-radius:10px;box-shadow:0 0 0 5px var(--k-accent-bg-strong);pointer-events:none;transition:top var(--k-dur-2) var(--k-ease),left var(--k-dur-2) var(--k-ease),width var(--k-dur-2) var(--k-ease),height var(--k-dur-2) var(--k-ease)}
.k-tour-anchor .k-tour-ring{inset:-5px}
.k-tour{position:absolute;width:262px;max-width:calc(100% - 24px);background:var(--k-bg);border:1px solid var(--k-line);border-radius:14px;box-shadow:0 18px 44px -14px rgba(16,24,40,.28);padding:16px;z-index:5;font-family:var(--k-font)}
.k-tour h4{margin:0 0 5px;font-size:14px;font-weight:650;color:var(--k-ink)}
.k-tour p{margin:0;font-size:13px;line-height:1.6;color:var(--k-ink-2)}
.k-tour-arrow{position:absolute;top:-6px;left:28px;width:11px;height:11px;background:var(--k-bg);border-left:1px solid var(--k-line);border-top:1px solid var(--k-line);transform:rotate(45deg)}
.k-tour.flip .k-tour-arrow{top:auto;bottom:-6px;transform:rotate(225deg)}
.k-tour-ft{display:flex;align-items:center;gap:8px;margin-top:14px}
.k-tour-dots{display:flex;gap:5px;margin-right:auto;align-items:center}
.k-tour-dots i{width:6px;height:6px;border-radius:99px;background:var(--k-zinc-300);display:block}
.k-tour-dots i.on{background:var(--k-core);width:16px}
/* KATA Tour — KATATour.start([{el, title, text}, ...]); Skip and Escape always work. */
(function () {
var card = null, ring = null, steps = [], idx = 0, target = null;
function ensure() {
if (card) return;
ring = document.createElement('div');
ring.className = 'k-tour-ring';
ring.hidden = true;
ring.style.position = 'fixed';
ring.style.zIndex = '90';
document.body.appendChild(ring);
card = document.createElement('div');
card.className = 'k-tour';
card.hidden = true;
card.style.position = 'fixed';
card.style.zIndex = '91';
card.setAttribute('role', 'dialog');
card.setAttribute('aria-label', 'Product tour');
card.innerHTML = '<span class="k-tour-arrow"></span><h4 data-t></h4><p data-x></p>' +
'<div class="k-tour-ft"><span class="k-tour-dots" data-d></span>' +
'<button type="button" class="btn btn-ghost btn-sm" data-skip>Skip</button>' +
'<button type="button" class="btn btn-pri btn-sm" data-next>Next</button></div>';
document.body.appendChild(card);
card.querySelector('[data-skip]').addEventListener('click', end);
card.querySelector('[data-next]').addEventListener('click', function () {
idx++;
if (idx >= steps.length) end(); else show();
});
document.addEventListener('keydown', function (e) {
if (!card || card.hidden) return;
if (e.key === 'Escape') end();
if (e.key === 'ArrowRight') card.querySelector('[data-next]').click();
});
window.addEventListener('resize', place);
window.addEventListener('scroll', place, true);
}
function place() {
if (!target || card.hidden) return;
var r = target.getBoundingClientRect();
ring.style.left = (r.left - 5) + 'px';
ring.style.top = (r.top - 5) + 'px';
ring.style.width = (r.width + 10) + 'px';
ring.style.height = (r.height + 10) + 'px';
var cw = card.offsetWidth, ch = card.offsetHeight;
var below = r.bottom + 14 + ch <= window.innerHeight - 10;
card.classList.toggle('flip', !below);
card.style.top = (below ? r.bottom + 14 : r.top - ch - 14) + 'px';
card.style.left = Math.max(10, Math.min(r.left, window.innerWidth - cw - 10)) + 'px';
var ax = r.left + r.width / 2 - card.offsetLeft - 6;
card.querySelector('.k-tour-arrow').style.left = Math.max(14, Math.min(ax, cw - 26)) + 'px';
}
function show() {
var s = steps[idx];
target = document.querySelector(s.el);
if (!target) { idx++; if (idx < steps.length) show(); else end(); return; }
card.querySelector('[data-t]').textContent = s.title;
card.querySelector('[data-x]').textContent = s.text;
var dots = card.querySelector('[data-d]');
dots.innerHTML = '';
steps.forEach(function (_, i) {
var d = document.createElement('i');
if (i === idx) d.className = 'on';
dots.appendChild(d);
});
card.querySelector('[data-next]').textContent = idx === steps.length - 1 ? 'Done' : 'Next';
card.hidden = false;
ring.hidden = false;
place();
card.querySelector('[data-next]').focus();
}
function end() {
if (!card) return;
card.hidden = true;
ring.hidden = true;
}
window.KATATour = {
start: function (s) { steps = s || []; idx = 0; ensure(); if (steps.length) show(); },
end: end
};
})();
Installation
CLI
node kata-ui.mjs add tour
Writes the part’s CSS and JS into your project. Dependencies are resolved and written with it. New to the CLI? See the CLI doc.
Manual
- Load
tokens.cssonce, before any part. - Copy the CSS tab into your stylesheet.
- Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.
Examples
The last step
Publishing is one click
That is the whole tour. Everything else explains itself — and this walkthrough will not appear again.
<link rel="stylesheet" href="/components/button/button.css">
<div class="k-tour-stage" style="min-height:300px">
<div style="display:flex;gap:8px;align-items:center">
<button class="btn btn-sec btn-sm" type="button">Projects</button>
<button class="btn btn-sec btn-sm" type="button">Team</button>
<span class="k-tour-anchor"><button class="btn btn-pri btn-sm" type="button">Publish</button><span class="k-tour-ring"></span></span>
</div>
<div class="k-tour" style="left:120px;top:78px" role="dialog" aria-label="Product tour">
<span class="k-tour-arrow" style="left:96px"></span>
<h4>Publishing is one click</h4>
<p>That is the whole tour. Everything else explains itself — and this walkthrough will not appear again.</p>
<div class="k-tour-ft"><span class="k-tour-dots"><i></i><i></i><i class="on"></i></span><button type="button" class="btn btn-ghost btn-sm">Skip</button><button type="button" class="btn btn-pri btn-sm">Done</button></div>
</div>
</div>Card above the target
Settings live down here
When there is no room below the target, the card flips above it and the arrow follows.
<div class="k-tour-stage" style="display:flex;flex-direction:column;min-height:284px">
<div class="k-tour flip" style="left:18px;top:6px" role="dialog" aria-label="Product tour">
<span class="k-tour-arrow" style="left:44px"></span>
<h4>Settings live down here</h4>
<p>When there is no room below the target, the card flips above it and the arrow follows.</p>
<div class="k-tour-ft"><span class="k-tour-dots"><i class="on"></i><i></i></span><button type="button" class="btn btn-ghost btn-sm">Skip</button><button type="button" class="btn btn-pri btn-sm">Next</button></div>
</div>
<div style="margin-top:auto;display:flex;gap:8px;align-items:center">
<span class="k-tour-anchor"><button class="btn btn-sec btn-sm" type="button">Settings</button><span class="k-tour-ring"></span></span>
<button class="btn btn-ghost btn-sm" type="button">Help</button>
</div>
</div>Usage
Introduce a surface once, in four steps at most: KATATour.start([{el, title, text}, …]). Each step rings its target and anchors the card to it. Skip is always on screen — a tour the reader cannot leave is a modal in disguise.
Accessibility
Focus moves into the card on each step and the dialog is labelled by its heading. Escape skips the whole tour, arrow-right advances, and nothing the tour points at is ever blocked by it.
Anatomy
The public surface of this part, read from its own source. Class names are the API — the CSS tab above is the full contract.
.k-tour-stage | class |
.k-tour-anchor | class |
.k-tour-ring | class |
.k-tour | class |
.k-tour-arrow | class |
.flip | class |
.k-tour-ft | class |
.k-tour-dots | class |
.on | class |
data-t | data hook |
data-x | data hook |
data-d | data hook |
data-skip | data hook |
data-next | data hook |
aria-label | ARIA |
Related parts
Alert Dialog beta
The decision that cannot be clicked away — for destructive, irreversible actions.
Command beta
The ⌘K palette: one input that searches actions and pages, driven from the keyboard.
Context Menu beta
The right-click menu, written as a component instead of an accident of the browser.
Drawer beta
A panel that slides in from the edge — detail and forms without losing the page.