SWAMP UI/COMPONENTS/FIREFLY TOUR

Firefly Tour

the kindest onboarding

A wandering firefly that guides first-time visitors through your page. Instead of modal overlays and aggressive arrows, a small glowing creature flies between elements and whispers what each one is. The opposite of Intro.js energy.

No dependencies
~3 KB
localStorage persist
Any selector as target
LIVE PREVIEW · CLICK START
A firefly will fly through 4 elements on this page →

When to use

Perfect for any site where new visitors need orientation: SaaS dashboards, indie products, complex tools, design portfolios. The firefly is friendly and ignorable — it doesn't block content, doesn't lock focus, doesn't shame users for not following.

Skip if your site is genuinely self-explanatory. A tour on a simple landing page is a sign of insecurity, not helpfulness.

EXAMPLE TARGET · TOUR STOP #2
A typical "thing on the page" that a tour might point at

1. HTML

No special markup — the tour targets any CSS selector you already have. Just give the elements you want to highlight an id or class.

<!-- Use any existing element with an id/class -->
<header id="hero"> ... </header>
<nav class="main-nav"> ... </nav>
<section id="pricing"> ... </section>

2. CSS (drop-in)

All visual classes the tour creates dynamically. Customize colors via CSS variables.

.swamp-tour-dim {
  position: fixed; inset: 0; z-index: 9500;
  background: rgba(0,0,0,.5);
  opacity: 0; transition: opacity .4s; pointer-events: none;
}
.swamp-tour-dim.show { opacity: 1; pointer-events: auto; }

.swamp-tour-highlight {
  position: absolute; z-index: 9501;
  border: 2px solid #ffd23f;
  border-radius: 14px;
  box-shadow: 0 0 0 4px rgba(255,210,63,.2);
  pointer-events: none;
  transition: all .6s cubic-bezier(.65,.05,.36,1);
}

.swamp-tour-firefly {
  position: absolute; z-index: 9502;
  width: 14px; height: 14px; border-radius: 50%;
  background: #ffd23f;
  box-shadow: 0 0 14px #ffd23f, 0 0 32px rgba(255,210,63,.6);
  transform: translate(-50%, -50%);
  transition: left .9s cubic-bezier(.65,.05,.36,1),
              top  .9s cubic-bezier(.65,.05,.36,1);
}

.swamp-tour-tooltip {
  position: absolute; z-index: 9503; max-width: 280px;
  background: #0f2e22; color: #f3eedd;
  border: 1px solid rgba(255,210,63,.4);
  border-radius: 14px; padding: 18px 20px 14px;
  box-shadow: 0 18px 50px -10px rgba(0,0,0,.8);
  transition: all .6s cubic-bezier(.65,.05,.36,1);
}

3. JavaScript

A single function swampTour({steps, key, autoStart}). Each step has a target selector + message.

// Swamp UI — Firefly Tour
function swampTour(opts) {
  const steps    = opts.steps || [];
  const key      = opts.key || 'swamp:tour:seen';
  const autoStart = opts.autoStart !== false;
  let idx = 0;

  function build() {
    const dim = document.createElement('div'); dim.className = 'swamp-tour-dim';
    const hl  = document.createElement('div'); hl.className = 'swamp-tour-highlight';
    const fly = document.createElement('div'); fly.className = 'swamp-tour-firefly';
    const tip = document.createElement('div'); tip.className = 'swamp-tour-tooltip';
    document.body.append(dim, hl, fly, tip);
    return {dim, hl, fly, tip};
  }

  function place(el, step) {
    const tgt = document.querySelector(step.target);
    if (!tgt) return;
    const r = tgt.getBoundingClientRect();
    const top = r.top + window.scrollY, left = r.left + window.scrollX;

    // highlight + scroll
    el.hl.style.cssText = 'top:' + (top - 4) + 'px;left:' + (left - 4) + 'px;'
                       + 'width:' + (r.width + 8) + 'px;height:' + (r.height + 8) + 'px;';
    tgt.scrollIntoView({behavior: 'smooth', block: 'center'});

    // firefly hovers at top-right corner of target
    el.fly.style.left = (left + r.width) + 'px';
    el.fly.style.top  = top + 'px';

    // tooltip below highlight
    el.tip.innerHTML =
      '<div class="tour-step-n">STEP ' + (idx+1) + ' / ' + steps.length + '</div>' +
      '<div class="tour-msg">' + step.message + '</div>' +
      '<div class="tour-actions">' +
        '<button class="tour-skip">skip</button>' +
        '<button class="tour-next">' + (idx === steps.length-1 ? 'DONE ✓' : 'NEXT →') + '</button>' +
      '</div>';
    el.tip.style.top  = (top + r.height + 16) + 'px';
    el.tip.style.left = Math.max(10, Math.min(window.innerWidth - 300, left)) + 'px';

    el.tip.querySelector('.tour-next').onclick = next;
    el.tip.querySelector('.tour-skip').onclick = end;
  }

  let els;
  function next() {
    idx++;
    if (idx >= steps.length) { end(); return; }
    place(els, steps[idx]);
  }
  function end() {
    localStorage.setItem(key, '1');
    Object.values(els).forEach(e => e.remove());
  }
  function start() {
    if (!steps.length) return;
    idx = 0; els = build();
    requestAnimationFrame(() => {
      els.dim.classList.add('show');
      place(els, steps[0]);
    });
  }

  if (autoStart && !localStorage.getItem(key)) start();
  return { start, end };
}

// === Usage ===
const tour = swampTour({
  key: 'mysite:tour:seen',
  autoStart: true,
  steps: [
    { target: '#hero',    message: '<b>Welcome!</b> This is the hero.' },
    { target: '#pricing', message: 'Our plans live here.' },
    { target: '#contact', message: 'And this is how to reach us.' },
  ],
});

// Optional: replay tour from a button
document.querySelector('#help-btn')?.addEventListener('click', tour.start);
EXAMPLE TARGET · TOUR STOP #4
The tip jar — where users can support you
Liked this? Feed Mr. Knife & Ms. Ling.
And buy their owner a milk tea.
🧋 TREAT THE SWAMP
🎉 You've reached the end of the garden!All 7 Swamp UI components are now live.
Back to catalog →