Rondleiding met hotspots Nieuw
Korte rondleiding door een klantomgeving: vier hotspots die zacht kloppen, een uitleg per stap met terug, volgende, overslaan en Esc, een spotlight op het onderdeel en focus die in de uitleg blijft en daarna terugkeert. Klik op een losse hotspot om alleen die uitleg te zien. Posities worden uitgerekend op de echte elementen, dus hij werkt ook op mobiel. Pulseren valt weg bij reduced motion. Bewaar in productie in localStorage of het profiel welke tips iemand al zag.
Openstaand€ 4.280,006 facturen, 1 te laat
Btw-aangifte Q3€ 1.932,40indienen voor 31 okt
Zo gebruik je het
- Plak de HTML op de plek waar het onderdeel moet komen.
- Zet de CSS in je stylesheet. Alle klassen hebben een eigen voorvoegsel, dus niets botst met je eigen opmaak.
- Voeg de JavaScript toe en roep init() aan met het element waar de HTML in staat.
<!-- Rondleiding met hotspots · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="ptr">
<div class="ptr__app">
<div class="ptr__side" role="navigation" aria-label="Menu (demo)">
<span class="ptr__brand">Kasboek</span>
<a href="#" aria-current="page">Overzicht</a><a href="#">Facturen</a><a href="#" data-tip="3">Uitgaven</a><a href="#">Btw</a>
</div>
<div class="ptr__main">
<div class="ptr__top">
<label class="ptr__search" data-tip="1"><span class="ptr__sr">Zoeken</span><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg><input type="search" placeholder="Zoek klant of factuur"></label>
<button type="button" class="ptr__new" data-tip="0">Nieuwe factuur</button>
</div>
<div class="ptr__cards">
<div class="ptr__card"><span>Openstaand</span><b>€ 4.280,00</b><small>6 facturen, 1 te laat</small></div>
<div class="ptr__card" data-tip="2"><span>Btw-aangifte Q3</span><b>€ 1.932,40</b><small>indienen voor 31 okt</small></div>
</div>
</div>
</div>
<div class="ptr__dots"></div>
<button type="button" class="ptr__start">Rondleiding starten</button>
<div class="ptr__ring" hidden></div>
<div class="ptr__pop" role="dialog" aria-labelledby="ptr-t" aria-describedby="ptr-d" hidden>
<p class="ptr__count"></p>
<h4 class="ptr__t" id="ptr-t" tabindex="-1"></h4>
<p class="ptr__d" id="ptr-d"></p>
<div class="ptr__btns">
<button type="button" class="ptr__skip">Overslaan</button>
<button type="button" class="ptr__b" data-d="-1">Terug</button>
<button type="button" class="ptr__b ptr__b--main" data-d="1">Volgende</button>
</div>
</div>
<p class="ptr__sr" aria-live="polite"></p>
</div>
</div>/* Rondleiding met hotspots · NedDev · eigen werk. */
.ptr{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.12);position:absolute;inset:0;overflow:hidden;font-family:'Switzer',system-ui,sans-serif;font-size:12.5px;line-height:1.4;color:var(--ink);background:#f4f3f1;container:ptr/inline-size;text-align:left}
.ptr *,.ptr *::before,.ptr *::after{box-sizing:border-box}
.ptr svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round}
.ptr__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);margin:0}
.ptr__app{height:100%;display:grid;grid-template-columns:96px 1fr}
.ptr__side{display:flex;flex-direction:column;gap:2px;padding:12px 8px;background:var(--ink);color:#fff}
.ptr__brand{font-weight:700;font-size:13px;padding:0 6px 10px}
.ptr__side a{padding:5px 6px;border-radius:4px;color:#d8d6d2;text-decoration:none;font-size:12px;transition:background-color .15s,color .15s}
.ptr__side a:hover{background:#262626;color:#fff}
.ptr__side a[aria-current="page"]{background:#fff;color:var(--ink);font-weight:600}
.ptr__main{min-width:0;padding:12px;display:flex;flex-direction:column;gap:10px}
.ptr__top{display:flex;gap:6px;align-items:center}
.ptr__search{flex:1;min-width:0;display:flex;align-items:center;gap:6px;height:30px;padding:0 8px;border:1px solid var(--line);border-radius:4px;background:#fff;color:var(--mut)}
.ptr__search input{flex:1;min-width:0;height:100%;border:0;background:none;font:500 12px/1 'Switzer',system-ui,sans-serif;color:var(--ink)}
.ptr__search input:focus{outline:none}
.ptr__search:focus-within{outline:2px solid var(--pop);outline-offset:1px}
.ptr__new{height:30px;padding:0 10px;border:0;border-radius:4px;background:var(--ink);color:#fff;font:600 12px/1 'Switzer',system-ui,sans-serif;cursor:pointer;white-space:nowrap}
.ptr__new:hover{background:#2a2a2a}
.ptr__cards{display:grid;grid-template-columns:1fr 1fr;gap:8px}
.ptr__card{display:flex;flex-direction:column;padding:10px;background:#fff;border:1px solid var(--line);border-radius:4px}
.ptr__card span{font-size:11.5px;color:var(--mut)}
.ptr__card b{font-size:17px;font-weight:700;font-variant-numeric:tabular-nums;letter-spacing:-.01em}
.ptr__card small{font-size:11px;color:var(--mut)}
.ptr__dots{position:absolute;inset:0;pointer-events:none}
.ptr__dot{position:absolute;width:24px;height:24px;margin:-12px 0 0 -12px;padding:0;border:0;background:none;pointer-events:auto;cursor:pointer;display:grid;place-items:center}
.ptr__dot::before{content:'';width:10px;height:10px;border-radius:4px;background:var(--pop);outline:2px solid #fff}
.ptr__dot::after{content:'';position:absolute;width:10px;height:10px;border-radius:4px;border:2px solid var(--pop);animation:ptr-pulse 1.8s ease-out infinite}
.ptr__dot.is-seen::before{background:#b5b2ad}
.ptr__dot.is-seen::after{display:none}
.ptr.is-touring .ptr__dot{visibility:hidden}
@keyframes ptr-pulse{from{transform:scale(1);opacity:.9}to{transform:scale(2.6);opacity:0}}
.ptr__start{position:absolute;right:10px;bottom:10px;height:28px;padding:0 10px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 12px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s,color .15s}
.ptr__start:hover{background:var(--ink);color:#fff}
.ptr.is-touring .ptr__start{visibility:hidden}
.ptr__ring{position:absolute;z-index:3;border-radius:4px;outline:2px solid var(--pop);outline-offset:2px;box-shadow:0 0 0 9999px rgba(10,10,10,.45);pointer-events:none;transition:top .2s,left .2s,width .2s,height .2s}
.ptr__ring[hidden]{display:none}
.ptr__pop{position:absolute;z-index:4;width:min(236px,calc(100% - 16px));padding:10px 12px;background:#fff;border:1px solid var(--ink);border-radius:4px;transition:top .2s,left .2s}
.ptr__pop[hidden]{display:none}
.ptr__count{margin:0;font-size:11px;color:var(--mut)}
.ptr__t{margin:1px 0 3px;font-size:13.5px;font-weight:600}
.ptr__t:focus{outline:none}
.ptr__d{margin:0 0 8px;font-size:12px;color:#333}
.ptr__btns{display:flex;align-items:center;gap:4px}
.ptr__skip{margin-right:auto;padding:4px 0;border:0;background:none;font:500 11.5px/1.2 'Switzer',system-ui,sans-serif;color:var(--mut);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.ptr__skip:hover{color:var(--ink)}
.ptr__b{height:26px;padding:0 9px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 11.5px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s}
.ptr__b:hover{background:#f4f3f1}
.ptr__b:disabled{opacity:.35;cursor:not-allowed}
.ptr__b--main{background:var(--ink);color:#fff}
.ptr__b--main:hover{background:#2a2a2a}
.ptr button:focus-visible,.ptr a:focus-visible{outline:2px solid var(--pop);outline-offset:2px}
@container ptr (max-width:380px){
.ptr__app{grid-template-columns:1fr;grid-template-rows:1fr auto}
.ptr__side{order:2;flex-direction:row;justify-content:space-around;padding:6px}
.ptr__brand{display:none}
.ptr__main{padding:10px}
.ptr__start{bottom:46px}
}
@media (prefers-reduced-motion:reduce){.ptr__dot::after{animation:none;display:none}.ptr__ring,.ptr__pop,.ptr *{transition:none!important}}// Rondleiding met hotspots · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.ptr'); if (!w) return;
const dots = w.querySelector('.ptr__dots'), ring = w.querySelector('.ptr__ring'), pop = w.querySelector('.ptr__pop');
const start = w.querySelector('.ptr__start'), sr = w.querySelector('.ptr__sr');
const S = [
{ t: 'Nieuwe factuur', d: 'Maak een factuur in een minuut. Klantgegevens, btw en betaallink met iDEAL vullen we voor je in.' },
{ t: 'Alles terugvinden', d: 'Zoek op klantnaam, factuurnummer of bedrag. Ook een deel van een bedrag werkt, zoals 4.280.' },
{ t: 'Btw-aangifte', d: 'Hier zie je wat je dit kwartaal afdraagt. Twee weken voor de deadline krijg je een herinnering.' },
{ t: 'Bonnetjes', d: 'Fotografeer een bon met je telefoon of mail hem door. Hij staat dan vanzelf bij Uitgaven.' }
];
const seen = new Set();
let cur = -1, single = false, back = null;
const tgt = i => w.querySelector('[data-tip="' + i + '"]');
w.querySelectorAll('.ptr__side a').forEach(a => a.addEventListener('click', e => e.preventDefault()));
dots.innerHTML = S.map((s, i) => '<button type="button" class="ptr__dot" data-i="' + i + '" aria-label="Uitleg: ' + s.t + '"></button>').join('');
function rel(el) { const a = el.getBoundingClientRect(), b = w.getBoundingClientRect(); return { x: a.left - b.left, y: a.top - b.top, w: a.width, h: a.height }; }
function placeDots() {
S.forEach((s, i) => { const r = rel(tgt(i)), d = dots.children[i]; d.style.left = (r.x + r.w - 4) + 'px'; d.style.top = (r.y + 4) + 'px'; d.classList.toggle('is-seen', seen.has(i)); });
}
function place() {
if (cur < 0) return;
const r = rel(tgt(cur)), W = w.clientWidth, H = w.clientHeight;
Object.assign(ring.style, { left: r.x + 'px', top: r.y + 'px', width: r.w + 'px', height: r.h + 'px' });
const pw = pop.offsetWidth, ph = pop.offsetHeight;
let top = r.y + r.h + 10;
if (top + ph > H - 6) top = r.y - ph - 10;
if (top < 6) top = Math.max(6, Math.min(H - ph - 6, r.y + r.h / 2 - ph / 2));
let left = Math.min(Math.max(8, r.x + r.w / 2 - pw / 2), W - pw - 8);
if (top < r.y + r.h && top + ph > r.y) left = r.x > W / 2 ? Math.max(8, r.x - pw - 10) : Math.min(W - pw - 8, r.x + r.w + 10);
pop.style.top = top + 'px'; pop.style.left = left + 'px';
}
function show(i) {
cur = i; seen.add(i);
pop.querySelector('.ptr__count').textContent = single ? 'Tip' : (i + 1) + ' van ' + S.length;
pop.querySelector('.ptr__t').textContent = S[i].t;
pop.querySelector('.ptr__d').textContent = S[i].d;
const b = pop.querySelector('[data-d="-1"]'), n = pop.querySelector('[data-d="1"]');
b.hidden = single; b.disabled = i === 0;
n.textContent = single || i === S.length - 1 ? 'Klaar' : 'Volgende';
pop.querySelector('.ptr__skip').hidden = single;
ring.hidden = false; pop.hidden = false; w.classList.add('is-touring');
place(); pop.querySelector('.ptr__t').focus();
}
function begin(i, one, from) { single = one; back = from; show(i); }
function end(msg) {
cur = -1; ring.hidden = true; pop.hidden = true; w.classList.remove('is-touring'); placeDots();
if (msg) sr.textContent = msg;
(back && w.contains(back) ? back : start).focus();
if (seen.size === S.length) start.textContent = 'Rondleiding opnieuw';
}
start.addEventListener('click', () => begin(0, false, start));
dots.addEventListener('click', e => { const d = e.target.closest('.ptr__dot'); if (d) begin(+d.dataset.i, true, d); });
pop.addEventListener('click', e => {
if (e.target.closest('.ptr__skip')) { end('Rondleiding overgeslagen. Je vindt hem terug onder de knop rechtsonder.'); return; }
const b = e.target.closest('[data-d]'); if (!b) return;
const n = cur + +b.dataset.d;
if (single || n >= S.length) end(single ? '' : 'Rondleiding afgerond.'); else if (n >= 0) show(n);
});
w.addEventListener('keydown', e => {
if (cur < 0) return;
if (e.key === 'Escape') { e.preventDefault(); end('Rondleiding gesloten.'); return; }
if (!single && (e.key === 'ArrowRight' || e.key === 'ArrowLeft')) { e.preventDefault(); const n = cur + (e.key === 'ArrowRight' ? 1 : -1); if (n >= 0 && n < S.length) show(n); return; }
if (e.key === 'Tab') {
const f = [...pop.querySelectorAll('button')].filter(x => !x.hidden && !x.disabled);
const i = f.indexOf(document.activeElement);
if (e.shiftKey && i <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && (i === f.length - 1 || i < 0)) { e.preventDefault(); f[i < 0 ? 0 : 0].focus(); }
}
});
if (window.ResizeObserver) new ResizeObserver(() => { placeDots(); place(); }).observe(w);
placeDots();
}
init(document.querySelector('.jouw-container'));
Meer uit componenten
Iets gezien dat je mooi vindt?
Zet het in je selectie en stuur hem op. Dan bouwen we je site eromheen, in jouw huisstijl.
Neem contact op →