Spaarkaart met stempel New
A digital loyalty card for a coffee bar: a wooden stamp floats with your cursor over the card, on a click it flies to the next box and presses a cup into it with a thud, slightly skewed like real ink. At ten stamps you get the free coffee. On a phone or with the keyboard, "Add stamp" does the same. For the customer card of a coffee bar, bakery, barber or car wash, or as a progress counter in a loyalty campaign.
Koffiebar Bonen in Utrecht
Bij tien stempels is je koffie gratis.
3 van 10
How to use it
- Paste the HTML where the components should appear.
- Add the CSS to your stylesheet. All classes are prefixed, so nothing clashes with your own styles.
- Add the JavaScript and call init() with the element that contains the HTML.
<!-- 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'));
More components
Seen something you like?
Add it to your selection and send it over. We build your site around it, in your brand.
Get in touch →