Sitezoeker (Ctrl K) New
Site search that opens with Ctrl K, Cmd K or the slash key: results grouped into pages, services and actions, arrow keys to move, Enter to open, Esc to close, recent searches and an empty state with one way out. Combobox + listbox with aria-activedescendant; opens with @starting-style (appears instantly without support). In production you fill the list from your sitemap or search index.
Druk op / of Ctrl K om te zoeken op de site.
↑↓ kiezen
Enter openen
How to use it
- Paste the HTML where the navigation & menus 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.
<!-- Sitezoeker (Ctrl K) · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="cmk is-open">
<div class="cmk__page" inert>
<div class="cmk__bar">
<span class="cmk__logo">Van Dam Installatie</span>
<button class="cmk__trigger" type="button" aria-haspopup="dialog" aria-expanded="true" aria-controls="cmk-dlg">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>
<span>Zoeken</span><kbd class="cmk__kbd cmk__mod">Ctrl</kbd><kbd class="cmk__kbd">K</kbd>
</button>
</div>
<p class="cmk__hint">Druk op <kbd class="cmk__kbd">/</kbd> of <kbd class="cmk__kbd cmk__mod">Ctrl</kbd> <kbd class="cmk__kbd">K</kbd> om te zoeken op de site.</p>
<p class="cmk__done" aria-live="polite"></p>
</div>
<div class="cmk__scrim"></div>
<div class="cmk__dlg" id="cmk-dlg" role="dialog" aria-modal="true" aria-label="Zoeken op de site">
<div class="cmk__field">
<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="11" cy="11" r="6.5"/><path d="m16 16 4.5 4.5"/></svg>
<input class="cmk__input" type="text" role="combobox" aria-expanded="true" aria-controls="cmk-list" aria-autocomplete="list" aria-label="Zoek een pagina, dienst of actie" placeholder="Zoek een pagina, dienst of actie" autocomplete="off" spellcheck="false">
<button class="cmk__x" type="button" aria-label="Zoeker sluiten">Esc</button>
</div>
<div class="cmk__list" id="cmk-list" role="listbox" aria-label="Zoekresultaten"></div>
<div class="cmk__foot">
<span><kbd class="cmk__kbd">↑</kbd><kbd class="cmk__kbd">↓</kbd> kiezen</span>
<span><kbd class="cmk__kbd">Enter</kbd> openen</span>
<button class="cmk__clear" type="button" hidden>Recent wissen</button>
</div>
<p class="cmk__sr" aria-live="polite"></p>
</div>
</div>
</div>/* Sitezoeker (Ctrl K) · NedDev · eigen werk. */
.cmk{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.14);--soft:#efedea;position:absolute;inset:0;overflow:hidden;font-family:'Switzer',system-ui,sans-serif;font-size:13px;line-height:1.4;color:var(--ink);background:#f4f3f1;text-align:left}
.cmk *,.cmk *::before,.cmk *::after{box-sizing:border-box}
.cmk svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;flex:none}
.cmk__page{padding:12px 14px}
.cmk__bar{display:flex;align-items:center;justify-content:space-between;gap:10px;padding-bottom:10px;border-bottom:1px solid var(--line)}
.cmk__logo{font-weight:700;font-size:14px;letter-spacing:-.01em}
.cmk__trigger{display:inline-flex;align-items:center;gap:6px;padding:6px 8px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 12px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s}
.cmk__trigger:hover{background:var(--soft)}
.cmk__hint{margin:14px 0 0;color:var(--mut);font-size:12.5px}
.cmk__done{margin:8px 0 0;font-weight:600;font-size:12.5px}
.cmk__kbd{display:inline-block;min-width:18px;padding:2px 4px;border:1px solid var(--line);border-bottom-width:2px;border-radius:4px;background:#fff;font:500 11px/1.2 'Switzer',system-ui,sans-serif;color:var(--ink);text-align:center}
.cmk__scrim{position:absolute;inset:0;background:rgba(10,10,10,.38);opacity:0;pointer-events:none;transition:opacity .16s}
.cmk.is-open .cmk__scrim{opacity:1;pointer-events:auto}
.cmk__dlg{position:absolute;left:50%;top:10px;width:min(380px,calc(100% - 20px));max-height:calc(100% - 20px);display:none;flex-direction:column;transform:translateX(-50%);background:#fff;border:1px solid var(--ink);border-radius:4px;overflow:hidden;opacity:1;translate:0 0;transition:opacity .16s ease,translate .16s ease}
.cmk.is-open .cmk__dlg{display:flex}
@starting-style{.cmk.is-open .cmk__dlg{opacity:0;translate:0 -6px}}
.cmk__field{display:flex;align-items:center;gap:8px;padding:0 8px 0 10px;border-bottom:1px solid var(--line)}
.cmk__input{flex:1;min-width:0;height:38px;border:0;background:none;font:500 13.5px/1 'Switzer',system-ui,sans-serif;color:var(--ink)}
.cmk__input::placeholder{color:#767676}
.cmk__input:focus{outline:none}
.cmk__field:focus-within{box-shadow:inset 0 -2px 0 var(--ink)}
.cmk__x{min-height:24px;min-width:32px;padding:3px 6px;border:1px solid var(--line);border-radius:4px;background:#fff;font:500 11px/1.2 'Switzer',system-ui,sans-serif;color:var(--mut);cursor:pointer}
.cmk__x:hover{color:var(--ink);border-color:var(--ink)}
.cmk__list{flex:1;min-height:0;overflow:auto;padding:4px 0;scrollbar-width:thin}
.cmk__group{padding:2px 0}
.cmk__gh{margin:0;padding:6px 12px 3px;font-size:11.5px;font-weight:600;color:var(--mut)}
.cmk__opt{display:flex;align-items:center;gap:9px;padding:6px 12px;cursor:pointer;color:var(--ink)}
.cmk__opt svg{width:15px;height:15px;color:var(--mut)}
.cmk__opt:hover{background:#f6f5f3}
.cmk__opt[aria-selected="true"]{background:var(--soft)}
.cmk__opt[aria-selected="true"] svg{color:var(--ink)}
.cmk__t{flex:1;min-width:0;overflow:hidden;text-overflow:ellipsis;white-space:nowrap}
.cmk__t mark{background:rgba(245,72,15,.2);color:inherit}
.cmk__s{flex:none;font-size:11.5px;color:var(--mut);white-space:nowrap}
.cmk__empty{padding:10px 12px 4px;color:var(--mut);font-size:12.5px;margin:0}
.cmk__foot{display:flex;align-items:center;gap:12px;padding:6px 10px;border-top:1px solid var(--line);font-size:11px;color:var(--mut)}
.cmk__foot .cmk__kbd{margin-right:2px}
.cmk__clear{margin-left:auto;min-height:24px;padding:4px 0;border:0;background:none;font:500 11px/1.2 'Switzer',system-ui,sans-serif;color:var(--ink);text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.cmk__clear:hover{color:var(--pd)}
.cmk button:focus-visible{outline:2px solid var(--pop);outline-offset:2px}
.cmk__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);margin:0}
@media (max-width:420px){.cmk__foot span:nth-child(2){display:none}}
@media (prefers-reduced-motion:reduce){.cmk__dlg,.cmk__scrim{transition:none}}// Sitezoeker (Ctrl K) · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.cmk'); if (!w) return;
const page = w.querySelector('.cmk__page'), trig = w.querySelector('.cmk__trigger');
const input = w.querySelector('.cmk__input'), list = w.querySelector('.cmk__list');
const clear = w.querySelector('.cmk__clear'), sr = w.querySelector('.cmk__sr'), done = w.querySelector('.cmk__done');
const isMac = /Mac|iPhone|iPad/.test(navigator.platform || navigator.userAgent);
if (isMac) w.querySelectorAll('.cmk__mod').forEach(k => { k.textContent = '⌘'; });
const IC = {
p: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 3h8l4 4v14H6z"/><path d="M14 3v4h4"/></svg>',
d: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M14.5 6.5a4 4 0 0 0-5 5L4 17l3 3 5.5-5.5a4 4 0 0 0 5-5l-2.5 2.5-2.5-.5-.5-2.5z"/></svg>',
a: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M13 3 5 14h6l-1 7 8-11h-6z"/></svg>',
r: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="8"/><path d="M12 8v4l3 2"/></svg>'
};
const DATA = [
{ g: 'Pagina\'s', i: 'p', t: 'Home', s: '/', k: 'start voorpagina' },
{ g: 'Pagina\'s', i: 'p', t: 'Projecten', s: '/projecten', k: 'portfolio werk referenties' },
{ g: 'Pagina\'s', i: 'p', t: 'Over ons', s: '/over-ons', k: 'team bedrijf monteurs' },
{ g: 'Pagina\'s', i: 'p', t: 'Werken bij', s: '/vacatures', k: 'vacature baan monteur' },
{ g: 'Pagina\'s', i: 'p', t: 'Contact en route', s: '/contact', k: 'adres openingstijden' },
{ g: 'Diensten', i: 'd', t: 'Cv-ketel vervangen', s: 'vanaf € 2.150', k: 'ketel combiketel verwarming' },
{ g: 'Diensten', i: 'd', t: 'Warmtepomp (hybride)', s: 'ISDE-subsidie', k: 'duurzaam subsidie' },
{ g: 'Diensten', i: 'd', t: 'Airco laten plaatsen', s: 'vanaf € 1.495', k: 'koelen split' },
{ g: 'Diensten', i: 'd', t: 'Lekkage opsporen', s: 'binnen 24 uur', k: 'lek water storing' },
{ g: 'Diensten', i: 'd', t: 'Onderhoud cv-ketel', s: '€ 89 per jaar', k: 'service abonnement storing' },
{ g: 'Acties', i: 'a', t: 'Bel ons: 010 123 4567', s: 'ma-vr 8:00-17:00', k: 'telefoon storing spoed' },
{ g: 'Acties', i: 'a', t: 'Offerte aanvragen', s: 'binnen 2 werkdagen', k: 'prijs kosten' },
{ g: 'Acties', i: 'a', t: 'Storing melden', s: '24/7', k: 'spoed kapot' },
{ g: 'Acties', i: 'a', t: 'Afspraak plannen', s: 'online agenda', k: 'monteur langs' }
];
let recent = ['warmtepomp subsidie', 'storing cv'];
let opts = [], act = -1, lastFocus = null;
const norm = s => s.toLowerCase().normalize('NFD').replace(/[̀-ͯ]/g, '');
const esc = s => s.replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
const hl = (t, q) => {
if (!q) return esc(t);
const i = norm(t).indexOf(norm(q));
return i < 0 ? esc(t) : esc(t.slice(0, i)) + '<mark>' + esc(t.slice(i, i + q.length)) + '</mark>' + esc(t.slice(i + q.length));
};
function render() {
const q = input.value.trim();
const groups = [];
if (!q) {
if (recent.length) groups.push({ g: 'Recent gezocht', items: recent.map(r => ({ i: 'r', t: r, s: '', recent: true })) });
const sug = DATA.filter(d => ['Cv-ketel vervangen', 'Storing melden', 'Offerte aanvragen'].includes(d.t));
groups.push({ g: 'Veel gezocht', items: sug });
} else {
const words = norm(q).split(/\s+/);
const hits = DATA.filter(d => { const h = norm(d.t + ' ' + d.k + ' ' + d.s); return words.every(x => h.includes(x)); })
.sort((a, b) => (norm(b.t).startsWith(norm(q)) ? 1 : 0) - (norm(a.t).startsWith(norm(q)) ? 1 : 0));
['Pagina\'s', 'Diensten', 'Acties'].forEach(g => { const it = hits.filter(h => h.g === g); if (it.length) groups.push({ g, items: it }); });
}
clear.hidden = !!q || !recent.length;
let html = '', n = 0;
opts = [];
if (q && !groups.length) {
html = '<p class="cmk__empty">Geen resultaat voor “' + esc(q) + '”.</p>';
groups.push({ g: '', items: [{ i: 'a', t: 'Stel je vraag aan ons team', s: 'antwoord binnen 1 werkdag', ask: q }] });
}
groups.forEach((gr, gi) => {
const gid = 'cmk-g' + gi;
html += '<div class="cmk__group" role="group"' + (gr.g ? ' aria-labelledby="' + gid + '"><p class="cmk__gh" id="' + gid + '" role="presentation">' + esc(gr.g) + '</p>' : ' aria-label="Uitweg">');
gr.items.forEach(it => {
html += '<div class="cmk__opt" role="option" id="cmk-o' + n + '" aria-selected="false" data-n="' + n + '">' + IC[it.i] +
'<span class="cmk__t">' + hl(it.t, it.recent || it.ask ? '' : q) + '</span>' + (it.s ? '<span class="cmk__s">' + esc(it.s) + '</span>' : '') + '</div>';
opts.push(it); n++;
});
html += '</div>';
});
list.innerHTML = html;
setActive(opts.length ? 0 : -1);
sr.textContent = q ? (opts.length && !opts[0].ask ? opts.length + ' resultaten' : 'Geen resultaten') : '';
}
function setActive(i) {
act = i;
list.querySelectorAll('.cmk__opt').forEach(o => o.setAttribute('aria-selected', String(+o.dataset.n === i)));
if (i < 0) { input.removeAttribute('aria-activedescendant'); return; }
const el = list.querySelector('#cmk-o' + i);
input.setAttribute('aria-activedescendant', 'cmk-o' + i);
if (el) el.scrollIntoView({ block: 'nearest' });
}
function choose(i) {
const it = opts[i]; if (!it) return;
if (it.recent) { input.value = it.t; render(); input.focus(); return; }
const q = input.value.trim();
if (q) recent = [q].concat(recent.filter(r => r !== q)).slice(0, 4);
done.textContent = it.ask ? 'Vraag doorgezet naar het contactformulier: “' + it.ask + '”' : 'Geopend: ' + it.t + (it.g === 'Pagina\'s' ? ' (' + it.s + ')' : '');
close(true);
}
function open() {
if (w.classList.contains('is-open')) { input.focus(); return; }
lastFocus = document.activeElement;
w.classList.add('is-open'); page.inert = true; trig.setAttribute('aria-expanded', 'true');
input.value = ''; render(); input.focus();
}
function close(focusBack) {
w.classList.remove('is-open'); page.inert = false; trig.setAttribute('aria-expanded', 'false');
if (focusBack !== false) (lastFocus && w.contains(lastFocus) && lastFocus !== input ? lastFocus : trig).focus();
}
trig.addEventListener('click', open);
w.querySelector('.cmk__x').addEventListener('click', () => close());
w.querySelector('.cmk__scrim').addEventListener('click', () => close());
clear.addEventListener('click', () => { recent = []; render(); input.focus(); sr.textContent = 'Recente zoekopdrachten gewist'; });
input.addEventListener('input', render);
list.addEventListener('mousemove', e => { const o = e.target.closest('.cmk__opt'); if (o && +o.dataset.n !== act) setActive(+o.dataset.n); });
list.addEventListener('click', e => { const o = e.target.closest('.cmk__opt'); if (o) choose(+o.dataset.n); });
input.addEventListener('keydown', e => {
if (e.key === 'ArrowDown' || e.key === 'ArrowUp') {
e.preventDefault(); if (!opts.length) return;
setActive((act + (e.key === 'ArrowDown' ? 1 : -1) + opts.length) % opts.length);
} else if (e.key === 'Enter') { e.preventDefault(); if (act >= 0) choose(act); }
});
w.addEventListener('keydown', e => {
if (!w.classList.contains('is-open')) return;
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
if (e.key === 'Tab') {
const f = [...w.querySelectorAll('.cmk__dlg input,.cmk__dlg button:not([hidden])')];
const i = f.indexOf(document.activeElement);
if (e.shiftKey && i <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && i === f.length - 1) { e.preventDefault(); f[0].focus(); }
}
});
// Sneltoets: in productie op document; in het Lab alleen als je over de demo hangt of erin werkt.
document.addEventListener('keydown', e => {
const here = w.contains(document.activeElement) || w.matches(':hover');
if (!here) return;
const typing = /INPUT|TEXTAREA|SELECT/.test((document.activeElement || {}).tagName || '') && document.activeElement !== input;
if ((e.key.toLowerCase() === 'k' && (e.ctrlKey || e.metaKey)) || (e.key === '/' && !typing && document.activeElement !== input)) { e.preventDefault(); open(); }
});
render();
}
init(document.querySelector('.jouw-container'));
More navigation & menus
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 →