Zeepbellen die je wegblaast New
Soap bubbles rise slowly and wobble a little; move your mouse or finger through them and they blow away in your direction, tap one and it pops. The "Blow" button gives everyone a gust of wind, keyboard included. For the hero of a daycare, playground, swimming pool, kids party company or laundrette. Canvas 2D; with reduced motion the bubbles hang still.
Kinderopvang Het Speelkwartier
Zwolle · open dag woensdag 13 november
How to use it
- Paste the HTML where the backgrounds & webgl 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.
<!-- Zeepbellen die je wegblaast · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="zpb">
<canvas class="zpb__cv" aria-hidden="true"></canvas>
<div class="zpb__txt"><p class="zpb__h">Kinderopvang Het Speelkwartier</p><p class="zpb__p">Zwolle · open dag woensdag 13 november</p></div>
<div class="zpb__acties"><a class="zpb__btn" href="#">Plan een rondleiding</a><button class="zpb__blaas" type="button">Blaas</button></div>
</div>
</div>/* Zeepbellen die je wegblaast · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.zpb{position:absolute;inset:0;background:#f4f3f1;font-family:Switzer,sans-serif;color:#0a0a0a;overflow:hidden}
.zpb__cv{position:absolute;inset:0;width:100%;height:100%;display:block;touch-action:pan-y}
.zpb__txt{position:absolute;left:16px;top:14px;right:16px;pointer-events:none}
.zpb__h{margin:0;font:600 18px/1.2 Switzer,sans-serif}
.zpb__p{margin:4px 0 0;font:400 14px/1.35 Switzer,sans-serif;color:#3a3a3a}
.zpb__acties{position:absolute;left:16px;bottom:14px;display:flex;gap:8px}
.zpb__btn{display:inline-flex;align-items:center;min-height:36px;padding:0 14px;border:0;border-radius:4px;font:600 14px/1 Switzer,sans-serif;cursor:pointer;text-decoration:none;background:#0a0a0a;color:#fff;transition:background-color .18s,color .18s}
.zpb__btn:hover{background:#F5480F;color:#0a0a0a}
.zpb__btn:focus-visible{outline:2px solid #F5480F;outline-offset:3px}
.zpb__blaas{display:inline-flex;align-items:center;min-height:36px;padding:0 14px;border:1px solid #0a0a0a;border-radius:4px;background:#fff;color:#0a0a0a;font:500 14px/1 Switzer,sans-serif;cursor:pointer}
.zpb__blaas:hover{border-color:#F5480F}.zpb__blaas:focus-visible{outline:2px solid #F5480F;outline-offset:3px}
@media (prefers-reduced-motion: reduce){.zpb__btn{transition:none}}// Zeepbellen die je wegblaast · © 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){
var reduce=!!(window.matchMedia&&matchMedia('(prefers-reduced-motion: reduce)').matches);
function fit(cv,ctx,cb){var dpr=Math.min(window.devicePixelRatio||1,2);function f(){var r=cv.getBoundingClientRect();var w=Math.max(1,Math.round(r.width)),h=Math.max(1,Math.round(r.height));if(cv.width===Math.round(w*dpr)&&cv.height===Math.round(h*dpr))return;cv.width=Math.round(w*dpr);cv.height=Math.round(h*dpr);if(ctx)ctx.setTransform(dpr,0,0,dpr,0,0);if(cb)cb(w,h,dpr);}if(window.ResizeObserver)new ResizeObserver(f).observe(cv);f();}
function loop(el,step){var on=false,raf=0,last=0;function fr(t){raf=requestAnimationFrame(fr);var dt=last?Math.min(.05,(t-last)/1000):.016;last=t;step(dt,t/1000);}function go(v){if(v&&!on){on=true;last=0;raf=requestAnimationFrame(fr);}else if(!v&&on){on=false;cancelAnimationFrame(raf);}}if(window.IntersectionObserver)new IntersectionObserver(function(es){go(es[0].isIntersecting);}).observe(el);else go(true);}
var w = root.querySelector('.zpb'), cv = w.querySelector('.zpb__cv'), ctx = cv.getContext('2d');
var W = 1, H = 1, bs = [], pops = [], t = 0, mx = -999, my = -999, pvx = 0, pvy = 0, lx = null, ly = null, lt = 0;
function mk(b, bottom) { b.r = 10 + Math.random() * 24; b.x = Math.random() * W; b.y = bottom ? H + b.r + Math.random() * 40 : Math.random() * H; b.vx = 0; b.vy = 0; b.ph = Math.random() * 6.283; b.up = 12 + Math.random() * 16; }
function draw() {
ctx.clearRect(0, 0, W, H);
for (var i = 0; i < bs.length; i++) {
var b = bs[i];
ctx.beginPath(); ctx.arc(b.x, b.y, b.r, 0, 6.283); ctx.fillStyle = 'rgba(255,255,255,.42)'; ctx.fill();
ctx.lineWidth = 1.1; ctx.strokeStyle = 'rgba(10,10,10,.24)'; ctx.stroke();
ctx.beginPath(); ctx.arc(b.x, b.y, b.r * .74, 3.5, 4.6); ctx.strokeStyle = 'rgba(255,255,255,.95)'; ctx.lineWidth = 2; ctx.stroke();
ctx.beginPath(); ctx.arc(b.x, b.y, b.r * .86, .25, 1.35); ctx.strokeStyle = 'rgba(245,72,15,.6)'; ctx.lineWidth = 1.5; ctx.stroke();
}
for (var j = 0; j < pops.length; j++) {
var p = pops[j], k = p.a / .35, rr = p.r * (1 + k * .6);
ctx.strokeStyle = 'rgba(10,10,10,' + (.5 * (1 - k)).toFixed(3) + ')'; ctx.lineWidth = 1.2;
for (var q = 0; q < 8; q++) { var an = q * .785 + p.r; ctx.beginPath(); ctx.moveTo(p.x + Math.cos(an) * rr * .8, p.y + Math.sin(an) * rr * .8); ctx.lineTo(p.x + Math.cos(an) * rr, p.y + Math.sin(an) * rr); ctx.stroke(); }
}
}
fit(cv, ctx, function (a, b) {
W = a; H = b; var n = Math.max(8, Math.round(W * H / 6500)); bs = [];
for (var i = 0; i < n; i++) { var bb = {}; mk(bb); bs.push(bb); }
draw();
});
cv.addEventListener('pointermove', function (e) {
var r = cv.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top, tn = performance.now();
if (lx != null) { var d = Math.max(8, tn - lt) / 1000; pvx = pvx * .5 + (x - lx) / d * .5; pvy = pvy * .5 + (y - ly) / d * .5; }
lx = mx = x; ly = my = y; lt = tn;
});
cv.addEventListener('pointerleave', function () { mx = my = -999; lx = ly = null; pvx = pvy = 0; });
cv.addEventListener('pointerdown', function (e) {
var r = cv.getBoundingClientRect(), x = e.clientX - r.left, y = e.clientY - r.top;
for (var i = bs.length - 1; i >= 0; i--) { var b = bs[i]; if (Math.hypot(b.x - x, b.y - y) < b.r + 6) { pops.push({ x: b.x, y: b.y, r: b.r, a: 0 }); mk(b, true); if (reduce) draw(); break; } }
});
w.querySelector('.zpb__blaas').addEventListener('click', function () { if (reduce) return; for (var i = 0; i < bs.length; i++) { bs[i].vx += 140 + Math.random() * 120; bs[i].vy -= 30 + Math.random() * 40; } });
w.querySelector('.zpb__btn').addEventListener('click', function (e) { e.preventDefault(); });
loop(w, function (dt) {
if (reduce) return; t += dt; pvx *= Math.exp(-4 * dt); pvy *= Math.exp(-4 * dt);
for (var i = 0; i < bs.length; i++) {
var b = bs[i], d = Math.hypot(b.x - mx, b.y - my);
if (d < 120) { var f = (1 - d / 120) * 1.4 * dt; b.vx += pvx * f; b.vy += pvy * f; }
var k = Math.exp(-1.1 * dt); b.vx *= k; b.vy *= k;
b.x += (b.vx + Math.sin(t * 1.3 + b.ph) * 9) * dt; b.y += (b.vy - b.up) * dt;
if (b.y < -b.r - 4 || b.x < -b.r - 40 || b.x > W + b.r + 40) mk(b, true);
}
for (var j = pops.length - 1; j >= 0; j--) { pops[j].a += dt; if (pops[j].a > .35) pops.splice(j, 1); }
draw();
});
}
init(document.querySelector('.jouw-container'));
More backgrounds & webgl
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 →