Combobox diensten en plaatsen New
Accessible autocomplete following the ARIA combobox pattern: searches services and places, shows a type icon per row with the match in bold, works with arrows, Enter and Esc, and has a proper no-results state. Demo data lives in the code; in production connect places to the PDOK Locatieserver.
Niets gevonden voor ""Zoek op een vak (bijv. schilder) of op je woonplaats.
Probeer:
How to use it
- Paste the HTML where the forms & inputs 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.
<!-- Combobox diensten en plaatsen · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="cbd">
<label class="cbd__label">Wat zoek je, en waar?</label>
<div class="cbd__field">
<svg class="cbd__mag" viewBox="0 0 20 20" aria-hidden="true"><circle cx="8.5" cy="8.5" r="5.5"/><path d="M13 13l4 4"/></svg>
<input class="cbd__input" type="text" role="combobox" aria-autocomplete="list" aria-expanded="false" autocomplete="off" spellcheck="false" placeholder="Dakdekker, Rotterdam, badkamer...">
<div class="cbd__pop" hidden><p class="cbd__group"></p><ul class="cbd__list" role="listbox"></ul><div class="cbd__none" hidden><svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="10.5" cy="10.5" r="6.5"/><path d="M15.5 15.5l5 5M8 8l5 5M13 8l-5 5"/></svg><p><b>Niets gevonden voor "<span class="cbd__nterm"></span>"</b><span>Zoek op een vak (bijv. schilder) of op je woonplaats.</span></p></div></div>
</div>
<p class="cbd__hints"><span>Probeer:</span><button type="button" class="cbd__hint">dak</button><button type="button" class="cbd__hint">rotter</button><button type="button" class="cbd__hint">badkamer</button><button type="button" class="cbd__hint">kozijn</button></p>
<p class="cbd__chosen" aria-live="polite"></p>
<span class="cbd__sr" aria-live="polite"></span>
</div>
</div>/* Combobox diensten en plaatsen · NedDev · eigen werk. */
.cbd{position:absolute;inset:0;container-type:size;box-sizing:border-box;padding:16px 18px;background:#f4f3f1;color:#0a0a0a;font-family:'Switzer',system-ui,sans-serif;text-align:left}
.cbd *,.cbd *::before,.cbd *::after{box-sizing:border-box}
.cbd__label{display:block;font:500 10px/1.2 'JetBrains Mono',ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;color:#5c5a57;margin-bottom:6px}
.cbd__field{position:relative}
.cbd__mag{position:absolute;left:12px;top:20px;width:15px;height:15px;transform:translateY(-50%);fill:none;stroke:#5c5a57;stroke-width:1.8;stroke-linecap:round;pointer-events:none;z-index:1}
.cbd__input{display:block;width:100%;height:40px;margin:0;padding:0 12px 0 36px;border:1px solid #cfccc7;border-radius:4px;background:#fff;font:inherit;font-size:14px;color:#0a0a0a;transition:border-color .15s,box-shadow .15s}
.cbd__input::placeholder{color:#8a8781}
.cbd__input:hover{border-color:#8a8781}
.cbd__input:focus-visible{outline:none;border-color:#0a0a0a;box-shadow:0 0 0 3px rgba(245,72,15,.35)}
.cbd__input[aria-expanded="true"]{border-radius:4px 4px 0 0}
.cbd__pop{position:absolute;left:0;right:0;top:40px;z-index:5;background:#fff;border:1px solid #0a0a0a;border-top-color:#e6e3df;border-radius:0 0 4px 4px;box-shadow:0 12px 28px rgba(10,10,10,.14);max-height:max(90px,calc(100cqh - 92px));overflow:auto;overscroll-behavior:contain}
.cbd__pop[hidden]{display:none}
.cbd__group{margin:0;padding:8px 12px 2px;font:500 10px/1.2 'JetBrains Mono',ui-monospace,monospace;letter-spacing:.08em;text-transform:uppercase;color:#5c5a57}
.cbd__group:empty{display:none}
.cbd__list{list-style:none;margin:0;padding:4px}
.cbd__opt{display:flex;align-items:center;gap:10px;padding:7px 8px;border-radius:3px;cursor:pointer;line-height:1.25}
.cbd__opt.is-active{background:#f4f3f1;box-shadow:inset 2px 0 0 #F5480F}
.cbd__ico{flex:none;width:26px;height:26px;border-radius:4px;background:#f4f3f1;display:grid;place-items:center}
.cbd__opt.is-active .cbd__ico{background:#fff}
.cbd__ico svg{width:15px;height:15px;fill:none;stroke:#0a0a0a;stroke-width:1.6;stroke-linecap:round;stroke-linejoin:round}
.cbd__txt{min-width:0;flex:1}
.cbd__name{display:block;font-size:14px;font-weight:400;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cbd__name b,.cbd__sub b{font-weight:700;color:#0a0a0a}
.cbd__sub{display:block;font-size:12px;color:#5c5a57;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.cbd__kind{flex:none;font:500 10px/1 'JetBrains Mono',ui-monospace,monospace;color:#5c5a57;letter-spacing:.04em}
.cbd__opt--all{border-top:1px solid #e6e3df;border-radius:0 0 3px 3px;margin-top:4px;font-size:13px;font-weight:600}
.cbd__opt--all .cbd__name{font-weight:600}
.cbd__none{display:flex;gap:10px;align-items:flex-start;padding:12px}
.cbd__none[hidden]{display:none}
.cbd__none svg{flex:none;width:22px;height:22px;fill:none;stroke:#B8360A;stroke-width:1.6;stroke-linecap:round}
.cbd__none p{margin:0;font-size:13px;line-height:1.4}
.cbd__none b{display:block;font-weight:600}
.cbd__none span{color:#5c5a57}
.cbd__hints{display:flex;flex-wrap:wrap;align-items:center;gap:6px;margin:10px 0 0;font:500 11px/1 'JetBrains Mono',ui-monospace,monospace;color:#5c5a57}
.cbd__hint{font:inherit;color:#0a0a0a;background:#fff;border:1px solid #cfccc7;border-radius:999px;padding:5px 9px;cursor:pointer;transition:border-color .15s,background .15s}
.cbd__hint:hover{border-color:#0a0a0a}
.cbd__hint:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.cbd__chosen{margin:12px 0 0;font-size:13px;min-height:18px}
.cbd__chosen b{font-weight:600}
.cbd__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}// Combobox diensten en plaatsen · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.cbd');if(!w)return;
var uid='cbd'+Math.random().toString(36).slice(2,8);
var input=w.querySelector('.cbd__input'),pop=w.querySelector('.cbd__pop'),list=w.querySelector('.cbd__list'),none=w.querySelector('.cbd__none'),group=w.querySelector('.cbd__group'),sr=w.querySelector('.cbd__sr'),chosen=w.querySelector('.cbd__chosen'),label=w.querySelector('.cbd__label');
input.id=uid+'-in';label.setAttribute('for',input.id);list.id=uid+'-lb';input.setAttribute('aria-controls',list.id);list.setAttribute('aria-label','Suggesties');
var IC={dienst:'<svg viewBox="0 0 20 20"><path d="M12.5 3.5a3.5 3.5 0 0 0-3.3 4.6L3.5 13.8a1.4 1.4 0 0 0 2 2l5.7-5.7a3.5 3.5 0 0 0 4.6-3.3l-2 .9-1.8-1.8z"/></svg>',plaats:'<svg viewBox="0 0 20 20"><path d="M10 17.5s5.5-5 5.5-9a5.5 5.5 0 0 0-11 0c0 4 5.5 9 5.5 9z"/><circle cx="10" cy="8.5" r="2"/></svg>',all:'<svg viewBox="0 0 20 20"><circle cx="8.5" cy="8.5" r="5.5"/><path d="M13 13l4 4"/></svg>'};
var D=[['dienst','Dakdekker','Lekkage, dakgoot, nieuw dak'],['dienst','Dakkapel plaatsen','Vanaf € 7.950 inclusief montage'],['dienst','Loodgieter','Storing, lekkage, afvoer'],['dienst','Elektricien','Groepenkast, laadpaal, storing'],['dienst','Schilder','Binnen en buiten, offerte in 48 uur'],['dienst','Stukadoor','Wanden, plafonds, sierpleister'],['dienst','Tegelzetter','Badkamer, keuken, vloer'],['dienst','Badkamer renoveren','Complete badkamer vanaf € 9.800'],['dienst','Hovenier','Tuinontwerp, bestrating, onderhoud'],['plaats','Rotterdam','Zuid-Holland · 126 vakmensen'],['plaats','Den Haag','Zuid-Holland · 98 vakmensen'],['plaats','Dordrecht','Zuid-Holland · 41 vakmensen'],['plaats','Delft','Zuid-Holland · 24 vakmensen'],['plaats','Utrecht','Utrecht · 112 vakmensen'],['plaats','Amersfoort','Utrecht · 37 vakmensen'],['plaats','Zwolle','Overijssel · 29 vakmensen'],['plaats','Dalfsen','Overijssel · 6 vakmensen'],['plaats',"'s-Hertogenbosch",'Noord-Brabant · 33 vakmensen'],['plaats','Leiden','Zuid-Holland · 31 vakmensen']];
function norm(s){return s.toLowerCase().normalize('NFD').replace(/[\u0300-\u036f]/g,'');}
function esc(s){return s.replace(/[&<>"]/g,function(c){return {'&':'&','<':'<','>':'>','"':'"'}[c];});}
function bold(t,q){if(!q)return esc(t);var i=norm(t).indexOf(q);if(i<0)return esc(t);return esc(t.slice(0,i))+'<b>'+esc(t.slice(i,i+q.length))+'</b>'+esc(t.slice(i+q.length));}
var opts=[],active=-1,open=false,closeT;
function render(){var raw=input.value.trim(),q=norm(raw),res;
if(!q){res=[D[0],D[2],D[3],D[9],D[13]];group.textContent='Veel gezocht';}
else{res=D.map(function(d){var n=norm(d[1]),s=norm(d[2]),sc=n.indexOf(q)===0?0:n.indexOf(' '+q)>-1?1:n.indexOf(q)>-1?2:s.indexOf(q)>-1?3:9;return {d:d,sc:sc};}).filter(function(x){return x.sc<9;}).sort(function(a,b){return a.sc-b.sc;}).slice(0,6).map(function(x){return x.d;});group.textContent='';}
list.innerHTML='';opts=[];active=-1;
res.forEach(function(d,i){var li=document.createElement('li');li.className='cbd__opt';li.id=uid+'-o'+i;li.setAttribute('role','option');li.setAttribute('aria-selected','false');li._d={type:d[0],name:d[1]};
li.innerHTML='<span class="cbd__ico" aria-hidden="true">'+IC[d[0]]+'</span><span class="cbd__txt"><span class="cbd__name">'+bold(d[1],q)+'</span><span class="cbd__sub">'+bold(d[2],q)+'</span></span><span class="cbd__kind" aria-hidden="true">'+d[0]+'</span>';
list.appendChild(li);opts.push(li);});
if(q&&res.length){var li=document.createElement('li');li.className='cbd__opt cbd__opt--all';li.id=uid+'-all';li.setAttribute('role','option');li.setAttribute('aria-selected','false');li._d={type:'all',name:raw};
li.innerHTML='<span class="cbd__ico" aria-hidden="true">'+IC.all+'</span><span class="cbd__txt"><span class="cbd__name">Alle resultaten voor "'+esc(raw)+'"</span></span>';list.appendChild(li);opts.push(li);}
none.hidden=!(q&&!res.length);list.hidden=!opts.length;w.querySelector('.cbd__nterm').textContent=raw;
setOpen(true);input.removeAttribute('aria-activedescendant');
sr.textContent=q?(res.length?res.length+' suggesties, gebruik pijltjes omhoog en omlaag':'Geen suggesties gevonden'):'';}
function setOpen(v){open=v;pop.hidden=!v;input.setAttribute('aria-expanded',v?'true':'false');if(!v){active=-1;input.removeAttribute('aria-activedescendant');}}
function setActive(i){if(!opts.length)return;if(active>-1&&opts[active]){opts[active].classList.remove('is-active');opts[active].setAttribute('aria-selected','false');}
active=(i+opts.length)%opts.length;var o=opts[active];o.classList.add('is-active');o.setAttribute('aria-selected','true');input.setAttribute('aria-activedescendant',o.id);
var pt=pop.scrollTop,ph=pop.clientHeight,ot=o.offsetTop+list.offsetTop,oh=o.offsetHeight;if(ot<pt)pop.scrollTop=ot-4;else if(ot+oh>pt+ph)pop.scrollTop=ot+oh-ph+4;}
function pick(o){var d=o._d;setOpen(false);
if(d.type==='all'){chosen.innerHTML='Zoeken op <b>"'+esc(d.name)+'"</b> in alle diensten en plaatsen';}
else{input.value=d.name;chosen.innerHTML='Gekozen: <b>'+esc(d.name)+'</b> · '+(d.type==='dienst'?'dienst':'plaats');}}
input.addEventListener('input',function(){chosen.textContent='';render();});
input.addEventListener('focus',function(){if(input.value.trim())render();});
input.addEventListener('keydown',function(e){var k=e.key;
if(k==='ArrowDown'){e.preventDefault();if(!open)render();setActive(active+1);}
else if(k==='ArrowUp'){e.preventDefault();if(!open)render();setActive(active<0?opts.length-1:active-1);}
else if(k==='Enter'){if(open&&active>-1){e.preventDefault();pick(opts[active]);}}
else if(k==='Escape'){if(open){e.preventDefault();setOpen(false);}else if(input.value){e.preventDefault();input.value='';chosen.textContent='';}}
else if(k==='Tab'){setOpen(false);}});
input.addEventListener('blur',function(){closeT=setTimeout(function(){setOpen(false);},120);});
list.addEventListener('mousedown',function(e){e.preventDefault();});
list.addEventListener('click',function(e){var o=e.target.closest('.cbd__opt');if(o)pick(o);});
list.addEventListener('mousemove',function(e){var o=e.target.closest('.cbd__opt');if(o&&opts.indexOf(o)!==active)setActive(opts.indexOf(o));});
w.querySelectorAll('.cbd__hint').forEach(function(b){b.addEventListener('click',function(){clearTimeout(closeT);input.value=b.textContent;chosen.textContent='';input.focus();render();});});
}
init(document.querySelector('.jouw-container'));
More forms & inputs
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 →