Spaarkaart met stempel Nieuw
Een digitale spaarkaart van een koffiebar: een houten stempel zweeft met je cursor mee over de kaart, bij een klik vliegt hij naar het volgende vakje en drukt er met een tik een kopje in, licht scheef zoals echte inkt. Bij tien stempels krijg je de gratis koffie. Op een telefoon of met het toetsenbord zet "Stempel zetten" hetzelfde in gang. Voor de klantenkaart van een koffiebar, bakker, kapper of wasstraat, of als voortgangsteller bij een loyaliteitsactie.
Koffiebar Bonen in Utrecht
Bij tien stempels is je koffie gratis.
3 van 10
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.
<!-- Spaarkaart met stempel · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="skf">
<div class="skf__kaart">
<p class="skf__kop">Koffiebar Bonen in Utrecht</p>
<p class="skf__sub">Bij tien stempels is je koffie gratis.</p>
<div class="skf__vakken" role="img" aria-label="3 van 10 stempels">
<span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span>
<span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span><span class="skf__vak"></span>
</div>
<div class="skf__rij"><button type="button" class="skf__knop">Stempel zetten</button><span class="skf__teller" aria-live="polite">3 van 10</span></div>
</div>
<svg class="skf__stempel" viewBox="0 0 44 56" aria-hidden="true">
<circle cx="22" cy="9" r="8" fill="#6b4a2b"/><rect x="18" y="14" width="8" height="16" rx="2" fill="#6b4a2b"/>
<rect x="6" y="29" width="32" height="14" rx="3" fill="#0a0a0a"/><rect x="8" y="43" width="28" height="7" rx="2" fill="#F5480F"/>
</svg>
</div>
</div>/* Spaarkaart met stempel · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.skf{position:relative;width:100%;height:100%;display:grid;place-items:center;padding:10px;box-sizing:border-box;background:#f4f3f1;font-family:Switzer,sans-serif;overflow:hidden}
.skf__kaart{width:100%;max-width:330px;background:#fff;border-radius:8px;padding:12px 16px;box-sizing:border-box;box-shadow:0 8px 24px rgba(10,10,10,.08)}
.skf__kaart.is-tik{animation:skf-tik .18s ease-out}
@keyframes skf-tik{50%{transform:translateY(2px) scale(.995)}}
.skf__kop{margin:0;font-size:16px;font-weight:600;color:#0a0a0a;line-height:1.2}
.skf__sub{margin:3px 0 10px;font-size:13px;color:#55524d}
.skf__vakken{display:grid;grid-template-columns:repeat(5,36px);gap:8px 10px;justify-content:start}
.skf__vak{position:relative;width:36px;height:36px;border-radius:50%;border:1.5px dashed #c9c6c0;box-sizing:border-box}
.skf__vak svg{position:absolute;inset:-3px;width:calc(100% + 6px);height:calc(100% + 6px)}
.skf__vak.is-nieuw svg{animation:skf-inkt .35s ease-out}
@keyframes skf-inkt{0%{opacity:0;transform:scale(1.25) rotate(var(--r))}100%{opacity:1;transform:scale(1) rotate(var(--r))}}
.skf__rij{display:flex;align-items:center;gap:12px;margin-top:10px}
.skf__knop{font:inherit;font-size:13px;font-weight:600;min-height:30px;padding:0 12px;border:0;border-radius:4px;background:#0a0a0a;color:#fff;cursor:pointer;transition:background-color .15s}
.skf__knop:hover{background:#F5480F}
.skf__knop:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.skf__teller{font-size:13px;color:#0a0a0a}
.skf__stempel{position:absolute;left:0;top:0;width:40px;height:51px;pointer-events:none;opacity:0;transition:opacity .2s;z-index:3;filter:drop-shadow(0 6px 6px rgba(10,10,10,.2))}
.skf.is-hover .skf__stempel,.skf.is-bezig .skf__stempel{opacity:1}
@media (hover:hover) and (pointer:fine){.skf.is-hover .skf__kaart{cursor:none}.skf.is-hover .skf__knop{cursor:pointer}}
@media (max-width:420px){.skf__vakken{grid-template-columns:repeat(5,32px);gap:6px 8px}.skf__vak{width:32px;height:32px}.skf__kaart{padding:10px 14px}}
@media (prefers-reduced-motion:reduce){.skf__kaart.is-tik,.skf__vak.is-nieuw svg{animation:none}.skf__stempel{transition:none}}// Spaarkaart met stempel · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.skf'); if (!w) return;
const kaart = w.querySelector('.skf__kaart'), vakken = [...w.querySelectorAll('.skf__vak')], stempel = w.querySelector('.skf__stempel');
const knop = w.querySelector('.skf__knop'), teller = w.querySelector('.skf__teller'), groep = w.querySelector('.skf__vakken');
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
let aantal = 0, bezig = false, muis = null, nu = { x: 0, y: 0 }, raf = 0;
const NS = 'http://www.w3.org/2000/svg';
function inkt(vak, nieuw){
const r = (Math.random() * 24 - 12).toFixed(1);
vak.style.setProperty('--r', r + 'deg');
vak.innerHTML = '<svg viewBox="0 0 42 42" style="transform:rotate(' + r + 'deg)"><circle cx="21" cy="21" r="18" fill="none" stroke="#F5480F" stroke-width="2.2" opacity=".9"/><path d="M13 17h13v7a6.5 6.5 0 0 1-13 0z" fill="#F5480F" opacity=".9"/><path d="M26 19h2.5a3 3 0 0 1 0 6H26" fill="none" stroke="#F5480F" stroke-width="2"/><path d="M12 31h17" stroke="#F5480F" stroke-width="2" stroke-linecap="round"/></svg>';
if (nieuw) { vak.classList.remove('is-nieuw'); void vak.offsetWidth; vak.classList.add('is-nieuw'); }
}
function bij(){
teller.textContent = aantal >= 10 ? 'Je volgende koffie is gratis' : aantal + ' van 10';
groep.setAttribute('aria-label', aantal + ' van 10 stempels');
knop.textContent = aantal >= 10 ? 'Nieuwe kaart' : 'Stempel zetten';
}
for (let i = 0; i < 3; i++) inkt(vakken[i], false);
aantal = 3; bij();
function zet(x, y){ nu = { x, y }; stempel.style.transform = 'translate(' + (x - 20).toFixed(1) + 'px,' + (y - 50).toFixed(1) + 'px)'; }
const binnen = el => { const a = w.getBoundingClientRect(), b = el.getBoundingClientRect(); return { x: b.left - a.left + b.width / 2, y: b.top - a.top + b.height / 2 }; };
function tween(van, naar, ms){
return new Promise(res => {
if (rm) { zet(naar.x, naar.y); res(); return; }
const t0 = performance.now();
(function f(t){ const k = Math.min(1, (t - t0) / ms), e = 1 - Math.pow(1 - k, 3); zet(van.x + (naar.x - van.x) * e, van.y + (naar.y - van.y) * e); if (k < 1) requestAnimationFrame(f); else res(); })(t0);
});
}
async function stempelen(){
if (bezig) return;
if (aantal >= 10) { vakken.forEach(v => { v.innerHTML = ''; v.classList.remove('is-nieuw'); }); aantal = 0; bij(); teller.textContent = 'Nieuwe kaart: 0 van 10'; return; }
bezig = true; w.classList.add('is-bezig');
const doel = binnen(vakken[aantal]);
const van = muis ? { x: muis.x, y: muis.y } : { x: w.clientWidth + 30, y: -10 };
if (!muis) zet(van.x, van.y);
await tween(van, { x: doel.x, y: doel.y - 18 }, 260);
await tween({ x: doel.x, y: doel.y - 18 }, { x: doel.x, y: doel.y + 4 }, 90);
inkt(vakken[aantal], true); aantal++; bij();
kaart.classList.remove('is-tik'); void kaart.offsetWidth; if (!rm) kaart.classList.add('is-tik');
await tween({ x: doel.x, y: doel.y + 4 }, { x: doel.x, y: doel.y - 22 }, 140);
const terug = muis ? { x: muis.x, y: muis.y } : { x: w.clientWidth + 30, y: -10 };
await tween(nu, terug, 220);
bezig = false; w.classList.remove('is-bezig');
}
w.addEventListener('pointermove', e => {
if (e.pointerType !== 'mouse') return;
const r = w.getBoundingClientRect(); muis = { x: e.clientX - r.left, y: e.clientY - r.top };
w.classList.add('is-hover'); if (!bezig) zet(muis.x, muis.y);
});
w.addEventListener('pointerleave', () => { muis = null; w.classList.remove('is-hover'); });
kaart.addEventListener('click', e => { if (e.target.closest('.skf__knop')) return; if (e.pointerType === 'mouse' || e.detail > 0) stempelen(); });
knop.addEventListener('click', stempelen);
}
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 →