Zaklamp op een donkere zolder New
A dark photo in which a torch beam follows mouse or finger; shine it on the cat and a joke appears, and "Lights on" shows the whole photo. While nobody moves, the beam wanders calmly by itself; the arrow keys steer it too. For the 404 page of a removal, storage or handyman company: the visitor is lost, but smiles and clicks through to the homepage.
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.
<!-- Zaklamp op een donkere zolder · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie -->
<div class="jouw-container">
<div class="zkl">
<div class="zkl__veld" tabindex="0" role="group" aria-label="Zolder in het donker. Beweeg de zaklamp met muis, vinger of de pijltjestoetsen.">
<img class="zkl__foto" src="https://neddev.com/lab-media/zaklamp-404-1.webp" alt="Zolder met verhuisdozen en een rode kat op een doos" width="960" height="597" loading="lazy" draggable="false">
<div class="zkl__donker" aria-hidden="true"></div>
</div>
<p class="zkl__vondst" aria-live="polite"></p>
<div class="zkl__tekst">
<p class="zkl__kop">Deze pagina is zoek</p>
<p class="zkl__sub">Schijn even rond op onze zolder.</p>
<div class="zkl__rij"><button type="button" class="zkl__aan" aria-pressed="false">Licht aan</button><a class="zkl__terug" href="#">Naar de homepage</a></div>
</div>
</div>
</div>/* Zaklamp op een donkere zolder · © 2026 NedDev BV · NedDev Lab-licentie (gratis gebruik, niet doorverkopen) · neddev.com/nl/lab/gebruik/#licentie */
.zkl{position:relative;width:100%;height:100%;overflow:hidden;background:#0a0a0a;font-family:Switzer,sans-serif}
.zkl__veld{position:absolute;inset:0;touch-action:pan-y;cursor:none;outline:none}
.zkl__veld:focus-visible{box-shadow:inset 0 0 0 3px #F5480F}
.zkl__foto{position:absolute;inset:0;width:100%;height:100%;object-fit:cover;user-select:none;-webkit-user-drag:none}
.zkl__donker{position:absolute;inset:0;background:radial-gradient(circle at var(--x,50%) var(--y,45%),rgba(10,10,10,0) 0,rgba(10,10,10,0) var(--r0,38px),rgba(10,10,10,.95) var(--r1,92px));transition:opacity .5s ease}
.zkl.is-aan .zkl__donker{opacity:0}
.zkl.is-aan .zkl__veld{cursor:default}
.zkl__tekst{position:absolute;left:10px;bottom:10px;max-width:calc(100% - 20px);padding:10px 12px;background:rgba(10,10,10,.82);border-radius:4px;color:#fff;box-sizing:border-box}
.zkl__kop{margin:0;font-size:16px;font-weight:600;line-height:1.2}
.zkl__sub{margin:2px 0 8px;font-size:13px;color:#d6d3ce}
.zkl__rij{display:flex;gap:10px;align-items:center;flex-wrap:wrap}
.zkl__aan{font:inherit;font-size:13px;min-height:28px;padding:0 10px;border-radius:4px;border:1px solid #fff;background:transparent;color:#fff;cursor:pointer;transition:background-color .15s,color .15s}
.zkl__aan:hover,.zkl__aan[aria-pressed="true"]{background:#fff;color:#0a0a0a}
.zkl__terug{font-size:13px;color:#fff;text-decoration:underline;text-underline-offset:3px;min-height:28px;display:inline-flex;align-items:center}
.zkl__terug:hover{color:#F5480F}
.zkl__aan:focus-visible,.zkl__terug:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.zkl__vondst{position:absolute;right:10px;top:10px;margin:0;max-width:60%;font-size:13px;line-height:1.3;color:#0a0a0a;background:#fff;border-radius:4px;padding:6px 9px;opacity:0;transform:translateY(-4px);transition:opacity .25s,transform .25s;pointer-events:none}
.zkl__vondst.is-zichtbaar{opacity:1;transform:none}
@media (prefers-reduced-motion:reduce){.zkl__donker,.zkl__vondst{transition:none}}// Zaklamp op een donkere zolder · © 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('.zkl'); if (!w) return;
const veld = w.querySelector('.zkl__veld'), donker = w.querySelector('.zkl__donker');
const vondst = w.querySelector('.zkl__vondst'), aan = w.querySelector('.zkl__aan');
const rm = matchMedia('(prefers-reduced-motion: reduce)').matches;
const KAT = { x: 0.53, y: 0.42 };
let doel = { x: 0.3, y: 0.4 }, nu = { x: 0.3, y: 0.4 }, zelf = !rm, raf = 0, zichtbaar = true, gevonden = false;
function zet(){
const r = veld.getBoundingClientRect();
const s = Math.max(1, Math.min(r.width, r.height) / 250);
donker.style.setProperty('--x', (nu.x * 100).toFixed(2) + '%');
donker.style.setProperty('--y', (nu.y * 100).toFixed(2) + '%');
donker.style.setProperty('--r0', Math.round(38 * s) + 'px');
donker.style.setProperty('--r1', Math.round(92 * s) + 'px');
const dx = (nu.x - KAT.x) * r.width, dy = (nu.y - KAT.y) * r.height;
const raak = Math.hypot(dx, dy) < 46 * s;
if (raak && !gevonden && !zelf){ gevonden = true; vondst.textContent = 'Gevonden: Mol, onze kantoorkat. De pagina helaas niet.'; vondst.classList.add('is-zichtbaar'); }
}
function tik(t){
raf = 0; if (!zichtbaar) return;
if (zelf){ doel = { x: 0.5 + 0.32 * Math.sin(t / 1900), y: 0.42 + 0.16 * Math.sin(t / 1300) }; }
nu.x += (doel.x - nu.x) * (rm ? 1 : 0.16); nu.y += (doel.y - nu.y) * (rm ? 1 : 0.16);
zet();
if (zelf || Math.abs(doel.x - nu.x) + Math.abs(doel.y - nu.y) > 0.001) raf = requestAnimationFrame(tik);
}
const start = () => { if (!raf) raf = requestAnimationFrame(tik); };
function naar(e){
const r = veld.getBoundingClientRect();
doel = { x: Math.min(1, Math.max(0, (e.clientX - r.left) / r.width)), y: Math.min(1, Math.max(0, (e.clientY - r.top) / r.height)) };
zelf = false; start();
}
veld.addEventListener('pointermove', naar);
veld.addEventListener('pointerdown', naar);
veld.addEventListener('keydown', e => {
const stap = e.shiftKey ? 0.12 : 0.05; const m = { ArrowLeft:[-stap,0], ArrowRight:[stap,0], ArrowUp:[0,-stap], ArrowDown:[0,stap] }[e.key];
if (!m) return; e.preventDefault(); zelf = false;
doel = { x: Math.min(1, Math.max(0, doel.x + m[0])), y: Math.min(1, Math.max(0, doel.y + m[1])) }; start();
});
aan.addEventListener('click', () => { const op = !w.classList.contains('is-aan'); w.classList.toggle('is-aan', op); aan.setAttribute('aria-pressed', String(op)); aan.textContent = op ? 'Licht uit' : 'Licht aan'; });
w.querySelector('.zkl__terug').addEventListener('click', e => e.preventDefault());
if ('IntersectionObserver' in window) new IntersectionObserver(es => { zichtbaar = es[0].isIntersecting; if (zichtbaar) start(); }).observe(w);
zet(); start();
}
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 →