Zwengel die door de jaren draait New
Two gears with a crank: turn it round with mouse or finger and the company years run along, from the first lathe until today; let go and the wheels keep turning a little. The gear is a slider, so the arrow keys (or Home and End) turn it too. For the about page of an engineering works, forge, watchmaker or family business with a long history.
1964
Sjoerd Hoekstra begint met twee draaibanken in een schuur in Leeuwarden.
Draai aan de zwengel of gebruik de pijltjes.
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.
<!-- Zwengel die door de jaren draait · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="zwg">
<svg class="zwg__wielen" viewBox="14 20 168 130" tabindex="0" role="slider" aria-label="Zwengel: draai door de geschiedenis" aria-valuemin="1964" aria-valuemax="2024" aria-valuenow="1964">
<g class="zwg__groot"><path class="zwg__tand-g" fill="#0a0a0a"/><circle cx="0" cy="0" r="9" fill="#f4f3f1"/><g class="zwg__gaten" fill="#f4f3f1"></g>
<rect x="-3" y="-38" width="6" height="38" rx="3" fill="#8a857e"/><circle class="zwg__knop" cx="0" cy="-38" r="8" fill="#F5480F"/></g>
<g class="zwg__klein"><path class="zwg__tand-k" fill="#F5480F"/><circle cx="0" cy="0" r="6" fill="#f4f3f1"/></g>
</svg>
<div class="zwg__info">
<p class="zwg__jaar">1964</p>
<p class="zwg__mijl" aria-live="polite">Sjoerd Hoekstra begint met twee draaibanken in een schuur in Leeuwarden.</p>
<p class="zwg__hint">Draai aan de zwengel of gebruik de pijltjes.</p>
</div>
</div>
</div>/* Zwengel die door de jaren draait · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.zwg{width:100%;height:100%;display:flex;align-items:center;justify-content:center;gap:14px;padding:12px 18px;box-sizing:border-box;background:#f4f3f1;font-family:Switzer,sans-serif}
.zwg__wielen{flex:0 0 auto;width:min(50%,240px);height:auto;cursor:grab;touch-action:none;outline:none;border-radius:8px}
.zwg__wielen:active{cursor:grabbing}
.zwg__wielen:focus-visible{box-shadow:0 0 0 2px #F5480F}
.zwg__info{flex:1 1 auto;min-width:0;max-width:200px}
.zwg__jaar{margin:0;font-size:40px;font-weight:700;line-height:1;color:#0a0a0a;font-variant-numeric:tabular-nums}
.zwg__mijl{margin:8px 0 0;font-size:13px;line-height:1.4;color:#0a0a0a;min-height:5.6em}
.zwg__mijl.is-nieuw{animation:zwg-in .35s ease-out}
@keyframes zwg-in{from{opacity:0;transform:translateY(6px)}to{opacity:1;transform:none}}
.zwg__hint{margin:6px 0 0;font-size:12px;color:#55524d}
@media (max-width:420px){.zwg{gap:10px;padding:10px 12px}.zwg__jaar{font-size:32px}.zwg__mijl{font-size:12px}}
@media (prefers-reduced-motion:reduce){.zwg__mijl.is-nieuw{animation:none}}// Zwengel die door de jaren draait · © 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('.zwg'); if (!w) return;
const svg = w.querySelector('.zwg__wielen'), groot = w.querySelector('.zwg__groot'), klein = w.querySelector('.zwg__klein');
const jaar = w.querySelector('.zwg__jaar'), mijl = w.querySelector('.zwg__mijl');
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const G = { x: 76, y: 88, r: 50, n: 18 }, K = { r: 30, n: 11 }, D = 7;
const hoek = -28 * Math.PI / 180, afst = G.r + K.r - D;
K.x = G.x + Math.cos(hoek) * afst; K.y = G.y + Math.sin(hoek) * afst;
function tanden(n, r){
const T = Math.PI * 2 / n; let s = '';
for (let i = 0; i < n; i++){
const a = i * T;
[[r - D, a], [r, a + 0.2 * T], [r, a + 0.5 * T], [r - D, a + 0.7 * T]].forEach((q, j) => {
s += (i === 0 && j === 0 ? 'M' : 'L') + (Math.cos(q[1]) * q[0]).toFixed(2) + ' ' + (Math.sin(q[1]) * q[0]).toFixed(2) + ' ';
});
}
return s + 'Z';
}
w.querySelector('.zwg__tand-g').setAttribute('d', tanden(G.n, G.r));
w.querySelector('.zwg__tand-k').setAttribute('d', tanden(K.n, K.r));
const gaten = w.querySelector('.zwg__gaten');
for (let i = 0; i < 5; i++){ const a = i * Math.PI * 2 / 5 + 0.6, c = document.createElementNS('http://www.w3.org/2000/svg', 'circle'); c.setAttribute('cx', (Math.cos(a) * 25).toFixed(2)); c.setAttribute('cy', (Math.sin(a) * 25).toFixed(2)); c.setAttribute('r', '7'); gaten.appendChild(c); }
const MIJL = [
[1964, 'Sjoerd Hoekstra begint met twee draaibanken in een schuur in Leeuwarden.'],
[1978, 'Eerste eigen hal aan de Hemrik, met vijf man in de werkplaats.'],
[1992, 'De eerste CNC-freesbank komt binnen; zoon Rients neemt de planning over.'],
[2008, 'Onderdelen voor melkrobots, inmiddels een derde van al het werk.'],
[2024, 'Machinefabriek Hoekstra: 62 vakmensen, drie generaties, nog altijd in Leeuwarden.']
];
const MAX = Math.PI * 2 * 4;
let tot = 0, vel = 0, raf = 0, sleep = null, huidig = 0;
function teken(){
groot.setAttribute('transform', 'translate(' + G.x + ' ' + G.y + ') rotate(' + (tot * 180 / Math.PI).toFixed(2) + ')');
klein.setAttribute('transform', 'translate(' + K.x.toFixed(2) + ' ' + K.y.toFixed(2) + ') rotate(' + (-tot * G.n / K.n * 180 / Math.PI + 180 / K.n + 10).toFixed(2) + ')');
const j = Math.round(1964 + 60 * tot / MAX); jaar.textContent = j; svg.setAttribute('aria-valuenow', String(j));
let m = 0; MIJL.forEach((q, i) => { if (j >= q[0]) m = i; });
svg.setAttribute('aria-valuetext', j + ': ' + MIJL[m][1]);
if (m !== huidig) { huidig = m; mijl.textContent = MIJL[m][1]; mijl.classList.remove('is-nieuw'); void mijl.offsetWidth; mijl.classList.add('is-nieuw'); }
}
const zet = t => { tot = Math.max(0, Math.min(MAX, t)); teken(); };
function hoekVan(e){ const r = svg.getBoundingClientRect(), s = r.width / 168; return Math.atan2(e.clientY - (r.top + (G.y - 20) * s), e.clientX - (r.left + (G.x - 14) * s)); }
function rol(){
raf = 0; if (sleep || rm) return;
vel *= 0.94; zet(tot + vel);
if (Math.abs(vel) > 0.002 && tot > 0 && tot < MAX) raf = requestAnimationFrame(rol);
}
svg.addEventListener('pointerdown', e => {
if (e.button > 0) return; e.preventDefault(); cancelAnimationFrame(raf); raf = 0;
sleep = { id: e.pointerId, a: hoekVan(e) }; vel = 0; svg.setPointerCapture(e.pointerId);
});
svg.addEventListener('pointermove', e => {
if (!sleep || e.pointerId !== sleep.id) return;
const a = hoekVan(e); let d = a - sleep.a; if (d > Math.PI) d -= Math.PI * 2; if (d < -Math.PI) d += Math.PI * 2;
sleep.a = a; vel = vel * 0.5 + d * 0.5; zet(tot + d);
});
const eind = () => { if (!sleep) return; sleep = null; vel = Math.max(-0.3, Math.min(0.3, vel)); if (!raf) raf = requestAnimationFrame(rol); };
svg.addEventListener('pointerup', eind); svg.addEventListener('pointercancel', eind);
svg.addEventListener('keydown', e => {
const stap = { ArrowRight: 0.26, ArrowUp: 0.26, ArrowLeft: -0.26, ArrowDown: -0.26, PageUp: Math.PI / 2, PageDown: -Math.PI / 2 }[e.key];
if (stap !== undefined) { e.preventDefault(); zet(tot + stap); }
else if (e.key === 'Home') { e.preventDefault(); zet(0); }
else if (e.key === 'End') { e.preventDefault(); zet(MAX); }
});
teken();
}
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 →