Bloemen die opbloeien waar je klikt New
Wherever you click or tap, a stem grows up from the ground and a flower opens, each time with different petals and colours; after twelve flowers the oldest wilts. The "Plant a flower" button does the same for keyboard users, and with reduced motion the flowers appear at once. For the thank-you page of a gardener, florist or garden centre, or the empty state of a wish list.
Bedankt, je aanvraag is binnen
Tuinontwerp De Linde komt dinsdag 12 november langs in Bussum. Tik ergens om een bloem te planten.
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.
<!-- Bloemen die opbloeien waar je klikt · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="blk">
<svg class="blk__veld" aria-hidden="true"></svg>
<div class="blk__tekst">
<p class="blk__kop">Bedankt, je aanvraag is binnen</p>
<p class="blk__sub">Tuinontwerp De Linde komt dinsdag 12 november langs in Bussum. Tik ergens om een bloem te planten.</p>
<button type="button" class="blk__knop">Plant een bloem</button>
</div>
<p class="blk__sr" aria-live="polite"></p>
</div>
</div>/* Bloemen die opbloeien waar je klikt · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.blk{position:relative;width:100%;height:100%;overflow:hidden;background:#f4f3f1;font-family:Switzer,sans-serif;cursor:pointer}
.blk::after{content:"";position:absolute;left:0;right:0;bottom:0;height:8px;background:#0a0a0a}
.blk__veld{position:absolute;inset:0;width:100%;height:100%}
.blk__tekst{position:relative;z-index:1;width:min(62%,250px);margin:14px 0 0 16px;padding:10px 12px;background:rgba(244,243,241,.92);border-radius:4px;cursor:default}
.blk__kop{margin:0;font-size:16px;font-weight:600;line-height:1.2;color:#0a0a0a}
.blk__sub{margin:4px 0 8px;font-size:13px;line-height:1.35;color:#55524d}
.blk__knop{font:inherit;font-size:13px;min-height:30px;padding:0 12px;border:0;border-radius:4px;background:#0a0a0a;color:#fff;cursor:pointer;transition:background-color .15s}
.blk__knop:hover{background:#F5480F}
.blk__knop:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.blk__sr{position:absolute;width:1px;height:1px;margin:0;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
@media (max-width:420px){.blk__tekst{margin:10px 0 0 10px;width:66%;padding:8px 10px}.blk__kop{font-size:15px}.blk__sub{font-size:12px}}// Bloemen die opbloeien waar je klikt · © 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('.blk'); if (!w) return;
const svg = w.querySelector('.blk__veld'), knop = w.querySelector('.blk__knop'), sr = w.querySelector('.blk__sr'), tekst = w.querySelector('.blk__tekst');
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const NS = 'http://www.w3.org/2000/svg';
const SOORTEN = [
{ blad:'#F5480F', hart:'#0a0a0a' }, { blad:'#ffffff', hart:'#F5480F' },
{ blad:'#ffb08f', hart:'#0a0a0a' }, { blad:'#0a0a0a', hart:'#F5480F' }
];
const bloemen = []; let raf = 0;
const mk = (n, a) => { const e = document.createElementNS(NS, n); for (const k in a) e.setAttribute(k, a[k]); return e; };
const uit = t => { const c1 = 1.9, c3 = c1 + 1; return 1 + c3 * Math.pow(t - 1, 3) + c1 * Math.pow(t - 1, 2); };
const glad = t => 1 - Math.pow(1 - t, 3);
function plant(x, y, direct){
const H = svg.clientHeight, grond = H - 8;
y = Math.min(grond - 24, Math.max(22, y));
const soort = SOORTEN[Math.floor(Math.random() * SOORTEN.length)];
const n = 5 + Math.floor(Math.random() * 4), r = 9 + Math.random() * 6, bocht = (Math.random() - .5) * 40;
const g = mk('g', {});
const bx = x + bocht * 0.3;
const steel = mk('path', { d: 'M' + bx + ' ' + grond + ' Q' + (x + bocht) + ' ' + ((grond + y) / 2) + ' ' + x + ' ' + y, fill: 'none', stroke: '#0a0a0a', 'stroke-width': 2, 'stroke-linecap': 'round' });
g.appendChild(steel);
const my = (grond + y) / 2, mx = (bx + x + bocht) / 2 - bocht * 0.25;
const blad1 = mk('path', { d: 'M0 0 Q9 -9 18 -2 Q9 3 0 0 Z', fill: '#0a0a0a' });
const blad2 = mk('path', { d: 'M0 0 Q-9 -9 -16 -2 Q-8 3 0 0 Z', fill: '#0a0a0a' });
g.appendChild(blad1); g.appendChild(blad2);
const kop = mk('g', {});
const fase = Math.random() * 360;
const blaadjes = [];
for (let i = 0; i < n; i++){
const b = mk('ellipse', { cx: 0, cy: -r * 0.62, rx: r * 0.42, ry: r * 0.62, fill: soort.blad, stroke: soort.blad === '#ffffff' ? '#d6d3ce' : 'none', 'stroke-width': 1 });
const bg = mk('g', {}); bg.appendChild(b); kop.appendChild(bg); blaadjes.push({ el: bg, hoek: fase + i * 360 / n });
}
const hart = mk('circle', { cx: 0, cy: 0, r: r * 0.32, fill: soort.hart });
kop.appendChild(hart); g.appendChild(kop); svg.appendChild(g);
const len = steel.getTotalLength();
const f = { g, steel, blad1, blad2, kop, hart, blaadjes, len, x, y, mx, my, t0: performance.now(), r, weg: 0 };
bloemen.push(f);
if (bloemen.length > 12) { const oud = bloemen.find(b => !b.weg); if (oud) oud.weg = performance.now(); }
teken(f, direct || rm ? 99999 : 0);
if (!direct && !rm) start();
}
function teken(f, ms){
const k = Math.min(1, ms / 520);
f.steel.setAttribute('stroke-dasharray', f.len); f.steel.setAttribute('stroke-dashoffset', (f.len * (1 - glad(k))).toFixed(2));
const kb = Math.max(0, Math.min(1, (ms - 300) / 380)), sb = kb ? uit(kb) : 0.001;
f.blad1.setAttribute('transform', 'translate(' + f.mx + ' ' + f.my + ') rotate(-20) scale(' + sb.toFixed(3) + ')');
f.blad2.setAttribute('transform', 'translate(' + (f.mx - 1) + ' ' + (f.my + 10) + ') rotate(18) scale(' + sb.toFixed(3) + ')');
f.kop.setAttribute('transform', 'translate(' + f.x + ' ' + f.y + ')');
f.blaadjes.forEach((b, i) => {
const kk = Math.max(0, Math.min(1, (ms - 460 - i * 45) / 420)), s = kk ? uit(kk) : 0.001;
b.el.setAttribute('transform', 'rotate(' + (b.hoek - 40 * (1 - kk)).toFixed(1) + ') scale(' + s.toFixed(3) + ')');
});
const kh = Math.max(0, Math.min(1, (ms - 420) / 300));
f.hart.setAttribute('r', (f.r * 0.32 * (kh ? uit(kh) : 0.001)).toFixed(2));
return k >= 1 && ms > 460 + f.blaadjes.length * 45 + 420;
}
function tik(t){
raf = 0; let bezig = false;
for (let i = bloemen.length - 1; i >= 0; i--){
const f = bloemen[i];
if (f.weg){
const k = Math.min(1, (t - f.weg) / 600); f.g.setAttribute('opacity', (1 - k).toFixed(3));
f.g.setAttribute('transform', 'translate(0 ' + (k * 6).toFixed(1) + ')');
if (k >= 1) { f.g.remove(); bloemen.splice(i, 1); } else bezig = true;
}
if (!f.klaar) { f.klaar = teken(f, t - f.t0); if (!f.klaar) bezig = true; }
}
if (bezig) raf = requestAnimationFrame(tik);
}
function start(){ if (!raf) raf = requestAnimationFrame(tik); }
function opruim(){ if (rm) { while (bloemen.length > 12) { bloemen.shift().g.remove(); } } }
w.addEventListener('pointerdown', e => {
if (tekst.contains(e.target)) return;
const r = svg.getBoundingClientRect(); plant(e.clientX - r.left, e.clientY - r.top); opruim();
});
knop.addEventListener('click', () => {
const W = svg.clientWidth, H = svg.clientHeight;
plant(W * (0.62 + Math.random() * 0.33), H * (0.35 + Math.random() * 0.4)); opruim();
sr.textContent = 'Er staat een nieuwe bloem in de tuin.';
});
function eerste(){
const W = svg.clientWidth, H = svg.clientHeight; if (!W) return false;
plant(W * 0.72, H * 0.52, true); plant(W * 0.86, H * 0.36, true); plant(W * 0.93, H * 0.66, true);
bloemen.forEach(f => { f.klaar = true; });
return true;
}
if (!eerste() && 'ResizeObserver' in window) { const ro = new ResizeObserver(() => { if (eerste()) ro.disconnect(); }); ro.observe(svg); }
}
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 →