Bestelling wordt ingepakt op scroll Nieuw
Een open doos in 3D-aanzicht: scrollen laat de zak koffie erin zakken, de flappen vouwen een voor een dicht, er loopt tape over de naad, het adreslabel plakt erop en er verschijnt een vinkje. Ernaast staat per stap wat er gebeurt. Voor de bedankpagina van een webshop ("wat gebeurt er nu") of een pagina over verzenden en duurzaam verpakken. Eigen SVG zonder libraries, eigen scrollvak met toetsenbord; bij minder beweging vallen de delen niet maar verschijnen ze.
Je bestelling, stap voor stap
Ligt klaarTwee zakken bonen, dinsdag vers gebrand in Deventer.
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.
<!-- Bestelling wordt ingepakt op scroll · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="pk">
<div class="pk__vak" tabindex="0" role="region" aria-label="Zo pakken we je bestelling in. Scroll in dit vak om de doos in te pakken.">
<div class="pk__baan">
<div class="pk__beeld">
<div class="pk__tekst">
<h3 class="pk__kop">Je bestelling, stap voor stap</h3>
<p class="pk__stap" aria-live="polite"><strong class="pk__naam">Ligt klaar</strong><span class="pk__uitleg">Twee zakken bonen, dinsdag vers gebrand in Deventer.</span></p>
</div>
<svg class="pk__svg" viewBox="0 0 240 190" role="img" aria-label="Doos die wordt ingepakt"></svg>
</div>
</div>
</div>
<p class="pk__hint" aria-hidden="true">Scroll om in te pakken</p>
</div>
</div>/* Bestelling wordt ingepakt op scroll · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.pk{position:absolute;inset:0;container-type:size;background:#f4f3f1;font-family:'Switzer',system-ui,sans-serif;color:#0a0a0a}
.pk__vak{position:absolute;inset:0;overflow-y:auto;scrollbar-width:none}
.pk__vak::-webkit-scrollbar{display:none}
.pk__vak:focus-visible{outline:2px solid #F5480F;outline-offset:-2px}
.pk__baan{height:500cqh}
.pk__beeld{position:sticky;top:0;height:100cqh;display:grid;grid-template-columns:minmax(0,1fr) minmax(0,1.3fr);align-items:center;gap:6px;padding:0 10px 0 16px;box-sizing:border-box}
.pk__kop{font-size:clamp(15px,4.6cqw,20px);font-weight:700;line-height:1.15;margin:0 0 10px}
.pk__stap{margin:0}
.pk__naam{display:block;font-size:clamp(13px,3.6cqw,16px);font-weight:600;color:#b83a0b}
.pk__uitleg{display:block;font-size:12px;line-height:1.4;color:#3d3d3d;margin-top:2px}
.pk__svg{width:100%;height:auto;max-height:94cqh;display:block}
.pk__hint{position:absolute;right:10px;bottom:8px;margin:0;font-size:12px;background:#fff;padding:3px 8px;border-radius:4px;pointer-events:none;transition:opacity .3s}
.pk.is-weg .pk__hint{opacity:0}// Bestelling wordt ingepakt op scroll · © 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('.pk'); if (!w) return;
const vak = w.querySelector('.pk__vak'), svg = w.querySelector('.pk__svg'), naam = w.querySelector('.pk__naam'), uitleg = w.querySelector('.pk__uitleg');
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const NS = 'http://www.w3.org/2000/svg';
const mk = (tag, at) => { const e = document.createElementNS(NS, tag); for (const k in at) e.setAttribute(k, at[k]); return e; };
const Wd = 100, D = 60, Hh = 56, ox = 62, oy = 168;
const P = (x, y, z) => [ox + x + z * .55, oy - y - z * .4];
const pts = a => a.map(q => P(q[0], q[1], q[2]).map(v => v.toFixed(1)).join(',')).join(' ');
const flapKl = '#d6aa78', lijn = { stroke: '#a57a4d', 'stroke-width': .8, 'stroke-linejoin': 'round' };
const achterFlap = mk('polygon', Object.assign({ fill: flapKl }, lijn));
const binnen = mk('polygon', { fill: '#7a5a3a', points: pts([[0, Hh, 0], [Wd, Hh, 0], [Wd, Hh, D], [0, Hh, D]]) });
const zak = mk('g', {});
zak.appendChild(mk('rect', { x: -15, y: -46, width: 30, height: 46, rx: 2, fill: '#0a0a0a' }));
zak.appendChild(mk('rect', { x: -15, y: -30, width: 30, height: 12, fill: '#F5480F' }));
zak.appendChild(mk('rect', { x: -12, y: -50, width: 24, height: 5, rx: 1, fill: '#2a2a2a' }));
const zak2 = mk('g', {});
zak2.appendChild(mk('rect', { x: -14, y: -40, width: 28, height: 40, rx: 2, fill: '#3a2a20' }));
zak2.appendChild(mk('rect', { x: -14, y: -26, width: 28, height: 10, fill: '#f4f3f1' }));
const links = mk('polygon', Object.assign({ fill: flapKl }, lijn)), rechts = mk('polygon', Object.assign({ fill: flapKl }, lijn));
const voorkant = mk('polygon', Object.assign({ fill: '#c99a68', points: pts([[0, 0, 0], [Wd, 0, 0], [Wd, Hh, 0], [0, Hh, 0]]) }, lijn));
const zijkant = mk('polygon', Object.assign({ fill: '#b08355', points: pts([[Wd, 0, 0], [Wd, 0, D], [Wd, Hh, D], [Wd, Hh, 0]]) }, lijn));
const voorFlap = mk('polygon', Object.assign({ fill: flapKl }, lijn));
const tape = mk('polygon', { fill: '#efe0b8', stroke: '#d8c38f', 'stroke-width': .6 });
const label = mk('g', {});
const L0 = P(50, 34, 0);
label.appendChild(mk('rect', { x: L0[0], y: L0[1], width: 40, height: 24, fill: '#fff', stroke: '#d9d5cf', 'stroke-width': .6 }));
label.appendChild(mk('rect', { x: L0[0], y: L0[1], width: 40, height: 5, fill: '#F5480F' }));
[9, 13.5, 18].forEach((dy, i) => label.appendChild(mk('rect', { x: L0[0] + 4, y: L0[1] + dy, width: i ? 24 : 30, height: 2, fill: '#5b5b5b' })));
const vink = mk('g', {});
vink.appendChild(mk('circle', { cx: 196, cy: 40, r: 13, fill: '#F5480F' }));
vink.appendChild(mk('path', { d: 'M189 40l5 5 9-10', fill: 'none', stroke: '#0a0a0a', 'stroke-width': 2.6, 'stroke-linecap': 'round', 'stroke-linejoin': 'round' }));
const grond = mk('ellipse', { cx: ox + Wd / 2 + 16, cy: oy + 4, rx: 82, ry: 7, fill: 'rgba(10,10,10,.08)' });
const cl = v => v < 0 ? 0 : v > 1 ? 1 : v, ease = t => 1 - Math.pow(1 - t, 3);
const rad = d => d * Math.PI / 180;
const flap = (el, hA, hB, v, len) => { el.setAttribute('points', pts([hA, hB, [hB[0] + v[0] * len, hB[1] + v[1] * len, hB[2] + v[2] * len], [hA[0] + v[0] * len, hA[1] + v[1] * len, hA[2] + v[2] * len]])); };
const S = [[0, 'Ligt klaar', 'Twee zakken bonen, dinsdag vers gebrand in Deventer.'], [.26, 'Doos dicht', 'Zonder plastic: alleen papier en karton.'], [.52, 'Tape erover', 'Papieren tape, mag gewoon mee met het oud papier.'], [.68, 'Label erop', 'Fam. de Wit, Zwolle. Morgen tussen 9:00 en 13:00.'], [.86, 'Onderweg', 'Je krijgt een mail met de track-and-trace.']];
let raf = 0, nu = -1;
function teken() {
raf = 0; const m = vak.scrollHeight - vak.clientHeight, p = m > 0 ? vak.scrollTop / m : 0;
const val = ease(cl(p / .2)), val2 = ease(cl((p - .07) / .16));
const sA = cl((p - .26) / .08), sB = cl((p - .34) / .08), sC = cl((p - .42) / .08);
const hoek = t => rad(120 - t * 120);
const aZ = hoek(sA), aB = hoek(sB), aV = rad(165 - sC * 165);
flap(achterFlap, [0, Hh, D], [Wd, Hh, D], [0, Math.sin(aB), -Math.cos(aB)], D / 2);
flap(links, [0, Hh, D], [0, Hh, 0], [Math.cos(aZ), Math.sin(aZ), 0], Wd / 2 - 2);
flap(rechts, [Wd, Hh, 0], [Wd, Hh, D], [-Math.cos(aZ), Math.sin(aZ), 0], Wd / 2 - 2);
flap(voorFlap, [0, Hh, 0], [Wd, Hh, 0], [0, Math.sin(aV), Math.cos(aV)], D / 2);
const c = P(Wd / 2 - 12, 16, D / 2), c2 = P(Wd / 2 + 16, 18, D / 2 + 6);
zak.setAttribute('transform', 'translate(' + c[0].toFixed(1) + ',' + (c[1] - (rm ? 0 : (1 - val) * 90)).toFixed(1) + ')'); zak.style.opacity = rm ? val : 1;
zak2.setAttribute('transform', 'translate(' + c2[0].toFixed(1) + ',' + (c2[1] - (rm ? 0 : (1 - val2) * 100)).toFixed(1) + ')'); zak2.style.opacity = rm ? val2 : (val2 > 0 ? 1 : 0);
const t = cl((p - .52) / .14), lang = (Wd + 30) * t, xe = Math.min(Wd, lang), neer = Math.max(0, lang - Wd);
tape.setAttribute('points', t <= 0 ? '' : pts([[0, Hh + .4, D / 2 - 6], [xe, Hh + .4, D / 2 - 6], [Wd + .3, Hh - neer, D / 2 - 6], [Wd + .3, Hh - neer, D / 2 + 6], [xe, Hh + .4, D / 2 + 6], [0, Hh + .4, D / 2 + 6]]));
const l = cl((p - .68) / .1); label.style.opacity = l;
label.style.transformOrigin = (L0[0] + 20) + 'px ' + (L0[1] + 12) + 'px';
label.style.transform = rm ? '' : 'scale(' + (1.25 - .25 * ease(l)).toFixed(3) + ')';
const v = cl((p - .86) / .08); vink.style.opacity = v; vink.style.transformOrigin = '196px 40px'; vink.style.transform = rm ? '' : 'scale(' + (.6 + .4 * ease(v)).toFixed(3) + ')';
const g = [grond, achterFlap, binnen, zak2, zak];
if (sB > .5) g.splice(1, 1);
g.push(links, rechts);
if (sB > .5) g.push(achterFlap);
g.push(voorkant, zijkant, voorFlap, tape, label, vink);
g.forEach(e => svg.appendChild(e));
let i = 0; S.forEach((s, j) => { if (p >= s[0]) i = j; });
if (i !== nu) { nu = i; naam.textContent = S[i][1]; uitleg.textContent = S[i][2]; }
w.classList.toggle('is-weg', vak.scrollTop > 8);
}
vak.addEventListener('scroll', () => { if (!raf) raf = requestAnimationFrame(teken); }, { passive: true });
teken();
}
init(document.querySelector('.jouw-container'));
Meer uit scroll-effecten
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 →