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.

Preview

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

  1. Load tokens.css once, before any part.
  2. Copy the CSS tab into your stylesheet.
  3. Copy the HTML where it belongs. The part has a script — include it after the markup; it initializes itself.

Examples

The last step

<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

<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-stageclass
.k-tour-anchorclass
.k-tour-ringclass
.k-tourclass
.k-tour-arrowclass
.flipclass
.k-tour-ftclass
.k-tour-dotsclass
.onclass
data-tdata hook
data-xdata hook
data-ddata hook
data-skipdata hook
data-nextdata hook
aria-labelARIA

Related parts