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.
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.
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>
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); }
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);