Bestelling wordt ingepakt op scroll New
An open box in 3D view: scrolling drops a bag of coffee in, the flaps fold shut one by one, tape runs over the seam, the address label sticks on and a check mark appears. Each step is explained alongside. For a webshop thank-you page ("what happens now") or a page about shipping and packaging. Own SVG without libraries, own scroll box with keyboard support.
Je bestelling, stap voor stap
Ligt klaarTwee zakken bonen, dinsdag vers gebrand in Deventer.
How to use it
- Paste the HTML where the scroll effects 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.
<!-- 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'));
More scroll effects
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 →