Tijdslotkiezer met dagdelen New
Standalone time slot picker for a call-back moment or delivery window: pick a day, then Morning/Afternoon/Evening tabs showing free counts, with half-hour windows as real radio buttons. Past times today and full windows are disabled; a day part without room shows an empty state with a button to the next free window. Tabs follow the ARIA tabs pattern (arrows, Home/End). Selected window via :has(:checked), with a JS class fallback.
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.
<!-- Tijdslotkiezer met dagdelen · NedDev · eigen werk. -->
<div class="jouw-container">
<form class="bts" novalidate><div class="bts__top"><p class="bts__q">Wanneer mogen we je terugbellen?</p><p class="bts__sub">Van Dam Installatie · binnen het gekozen half uur</p></div><fieldset class="bts__days"><legend class="bts__vh">Dag</legend></fieldset><div class="bts__tabs" role="tablist" aria-label="Dagdeel"><button type="button" role="tab" class="bts__tab" id="bts-t0" aria-controls="bts-p" data-p="0">Ochtend <span></span></button><button type="button" role="tab" class="bts__tab" id="bts-t1" aria-controls="bts-p" data-p="1">Middag <span></span></button><button type="button" role="tab" class="bts__tab" id="bts-t2" aria-controls="bts-p" data-p="2">Avond <span></span></button></div><div class="bts__panel" id="bts-p" role="tabpanel" tabindex="-1"></div><div class="bts__foot"><p class="bts__sum" aria-live="polite">Kies een tijdvak.</p><button type="submit" class="bts__go" disabled>Bel mij terug</button></div></form>
</div>/* Tijdslotkiezer met dagdelen · NedDev · eigen werk. */
.bts{position:absolute;inset:0;display:flex;flex-direction:column;gap:7px;margin:0;padding:10px 12px;background:#fff;font-family:'Switzer',system-ui,sans-serif;color:#0a0a0a;overflow:auto;container-type:inline-size}
.bts__vh{position:absolute;width:1px;height:1px;overflow:hidden;clip:rect(0 0 0 0);white-space:nowrap}
.bts__q{margin:0;font-size:14px;font-weight:700;line-height:1.2}
.bts__sub{margin:1px 0 0;font-size:11px;color:#5c5a56}
.bts__days{display:flex;gap:5px;margin:0;padding:0;border:0;min-width:0}
.bts__day{position:relative;flex:1;min-width:0}
.bts__day input{position:absolute;opacity:0;inset:0;margin:0;cursor:pointer}
.bts__day span{display:block;padding:4px 2px;border:1px solid rgba(10,10,10,.22);border-radius:4px;text-align:center;font-size:11.5px;line-height:1.2;transition:background-color .15s,border-color .15s,color .15s}
.bts__day b{display:block;font-size:10px;font-weight:500;color:#5c5a56;font-family:'JetBrains Mono',monospace;text-transform:uppercase;letter-spacing:.04em}
.bts__day:hover span{border-color:#0a0a0a}
.bts__day input:focus-visible+span{outline:2px solid #0a0a0a;outline-offset:2px}
.bts__day input:checked+span,.bts__day.is-on span{background:#0a0a0a;border-color:#0a0a0a;color:#fff}
.bts__day input:checked+span b,.bts__day.is-on span b{color:#d6d3cd}
.bts__tabs{display:flex;border-bottom:1px solid rgba(10,10,10,.14)}
.bts__tab{flex:1;height:28px;border:0;border-bottom:2px solid transparent;margin-bottom:-1px;background:none;font:600 12.5px 'Switzer',system-ui,sans-serif;color:#5c5a56;cursor:pointer;transition:color .15s,border-color .15s}
.bts__tab span{font:500 10.5px 'JetBrains Mono',monospace;color:#76736d}
.bts__tab:hover{color:#0a0a0a}
.bts__tab:focus-visible{outline:2px solid #0a0a0a;outline-offset:-2px}
.bts__tab[aria-selected="true"]{color:#0a0a0a;border-bottom-color:#F5480F}
.bts__panel{display:grid;grid-template-columns:repeat(5,minmax(0,1fr));gap:5px;align-content:start;min-height:72px}
.bts__panel:focus-visible{outline:2px solid #0a0a0a;outline-offset:2px}
.bts__slot{position:relative}
.bts__slot input{position:absolute;inset:0;opacity:0;margin:0;cursor:pointer}
.bts__slot span{display:block;padding:5px 2px;border:1px solid rgba(10,10,10,.22);border-radius:4px;text-align:center;font-size:12.5px;font-weight:600;font-variant-numeric:tabular-nums;line-height:1.15;transition:background-color .15s,border-color .15s,color .15s}
.bts__slot small{display:block;font-size:9.5px;font-weight:500;color:#5c5a56}
.bts__slot:hover span{border-color:#0a0a0a;background:#f4f3f1}
.bts__slot input:focus-visible+span{outline:2px solid #0a0a0a;outline-offset:2px}
.bts__slot input:checked+span,.bts__slot.is-on span{background:#0a0a0a;border-color:#0a0a0a;color:#fff}
.bts__slot input:checked+span small,.bts__slot.is-on span small{color:#d6d3cd}
.bts__slot input:disabled{cursor:not-allowed}
.bts__slot input:disabled+span{border-style:dashed;color:#9a978f;background:#fafaf9;text-decoration:line-through}
.bts__slot input:disabled+span small{text-decoration:none}
.bts__empty{grid-column:1/-1;margin:0;padding:9px 10px;border:1px dashed rgba(10,10,10,.25);border-radius:4px;font-size:12px;line-height:1.4;color:#5c5a56}
.bts__empty button{display:inline;margin-left:4px;padding:0;border:0;background:none;font:600 12px 'Switzer',system-ui,sans-serif;color:#0a0a0a;text-decoration:underline;text-underline-offset:2px;cursor:pointer}
.bts__empty button:focus-visible{outline:2px solid #0a0a0a;outline-offset:2px}
.bts__foot{display:flex;align-items:center;gap:8px;margin-top:auto}
.bts__sum{flex:1;margin:0;font-size:11.5px;line-height:1.3;color:#5c5a56;min-width:0}
.bts.is-done .bts__sum{color:#1f7a3d;font-weight:600}
.bts__go{flex:none;height:32px;padding:0 14px;border:1px solid #0a0a0a;border-radius:4px;background:#0a0a0a;color:#fff;font:600 12.5px 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s,color .15s,border-color .15s}
.bts__go:hover:not(:disabled){background:#F5480F;border-color:#F5480F;color:#0a0a0a}
.bts__go:active:not(:disabled){transform:translateY(1px)}
.bts__go:focus-visible{outline:2px solid #0a0a0a;outline-offset:2px}
.bts__go:disabled{background:#e9e7e3;border-color:#e9e7e3;color:#76736d;cursor:not-allowed}
.bts__go.is-busy{color:transparent;position:relative}
.bts__go.is-busy::after{content:'';position:absolute;inset:0;margin:auto;width:14px;height:14px;border:2px solid #fff;border-right-color:transparent;border-radius:50%;animation:bts-spin .7s linear infinite}
@keyframes bts-spin{to{transform:rotate(360deg)}}
@container (max-width:360px){.bts__panel{grid-template-columns:repeat(4,minmax(0,1fr))}}
@media (prefers-reduced-motion:reduce){.bts *{transition:none!important}.bts__go.is-busy::after{animation-duration:2s}}// Tijdslotkiezer met dagdelen · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var f=root.querySelector('.bts');if(!f)return;
var days=f.querySelector('.bts__days'),tabs=[].slice.call(f.querySelectorAll('.bts__tab')),panel=f.querySelector('.bts__panel'),sum=f.querySelector('.bts__sum'),go=f.querySelector('.bts__go');
var DAG=['zo','ma','di','wo','do','vr','za'],DAGL=['zondag','maandag','dinsdag','woensdag','donderdag','vrijdag','zaterdag'],MK=['jan','feb','mrt','apr','mei','jun','jul','aug','sep','okt','nov','dec'];
var PARTS=[[8,12],[12,17],[17,20]],NAMES=['ochtend','middag','avond'];
var T=new Date();T.setHours(0,0,0,0);
function add(d,n){var x=new Date(d);x.setDate(x.getDate()+n);return x}
function hm(m){return ('0'+(m/60|0)).slice(-2)+':'+('0'+m%60).slice(-2)}
/* 4 belbare dagen, zondag overgeslagen */
var list=[],d=new Date(T);while(list.length<4){if(d.getDay()!==0)list.push(new Date(d));d=add(d,1)}
days.insertAdjacentHTML('beforeend',list.map(function(x,i){var top=i===0&&+x===+T?'vandaag':(+x===+add(T,1)?'morgen':DAG[x.getDay()]);return '<label class="bts__day"><input type="radio" name="bts-day" value="'+i+'"'+(i===0?' checked':'')+'><span><b>'+top+'</b>'+x.getDate()+' '+MK[x.getMonth()]+'</span></label>'}).join(''));
function slots(day,p){var out=[],r=day.getDate()*7+p*3,now=new Date(),mins=now.getHours()*60+now.getMinutes();
if(p===2&&day.getDay()===6)return out; /* zaterdagavond niet */
for(var m=PARTS[p][0]*60;m<PARTS[p][1]*60;m+=30){r=(r*31+11)%97;var past=+day===+T&&m<=mins+30,full=r%5===0;out.push({m:m,off:past||full,why:past?'verstreken':'vol'})}return out}
function freeCount(day,p){return slots(day,p).filter(function(s){return !s.off}).length}
var dayIdx=0,part=0;
function curDay(){return list[dayIdx]}
function setTab(p,focus){part=p;tabs.forEach(function(t,i){var on=i===p;t.setAttribute('aria-selected',on?'true':'false');t.tabIndex=on?0:-1});panel.setAttribute('aria-labelledby','bts-t'+p);if(focus)tabs[p].focus();renderPanel()}
function counts(){tabs.forEach(function(t,i){t.querySelector('span').textContent=freeCount(curDay(),i)})}
function nextFree(){for(var di=dayIdx;di<list.length;di++)for(var p=(di===dayIdx?part:0);p<3;p++)if(freeCount(list[di],p))return [di,p];return null}
function renderPanel(){var s=slots(curDay(),part),free=s.filter(function(x){return !x.off}).length;
if(!free){var nf=nextFree();panel.innerHTML='<p class="bts__empty">'+(s.length?'Alle tijden in de '+NAMES[part]+' zijn bezet of voorbij.':'Op zaterdagavond bellen we niet.')+(nf?'<button type="button" class="bts__nf" data-d="'+nf[0]+'" data-p="'+nf[1]+'">Eerstvolgende vrije tijd</button>':'')+'</p>';update();return}
panel.innerHTML='<fieldset style="display:contents"><legend class="bts__vh">Tijdvak '+NAMES[part]+'</legend>'+s.map(function(x){return '<label class="bts__slot"><input type="radio" name="bts-slot" value="'+x.m+'"'+(x.off?' disabled':'')+' aria-label="'+hm(x.m)+' tot '+hm(x.m+30)+(x.off?', '+x.why:'')+'"><span>'+hm(x.m)+'<small>'+(x.off?x.why:'tot '+hm(x.m+30))+'</small></span></label>'}).join('')+'</fieldset>';update()}
function update(){var c=panel.querySelector('input:checked');f.querySelectorAll('.bts__slot,.bts__day').forEach(function(l){l.classList.toggle('is-on',l.querySelector('input').checked)});
f.classList.remove('is-done');go.disabled=!c;go.textContent='Bel mij terug';
if(c){var m=+c.value,dd=curDay();sum.textContent=DAGL[dd.getDay()]+' '+dd.getDate()+' '+MK[dd.getMonth()]+', tussen '+hm(m)+' en '+hm(m+30)+'.'}else sum.textContent='Kies een tijdvak.'}
days.addEventListener('change',function(e){dayIdx=+e.target.value;counts();var p=part;if(!freeCount(curDay(),p)){for(var i=0;i<3;i++)if(freeCount(curDay(),i)){p=i;break}}setTab(p,false)});
tabs.forEach(function(t,i){t.addEventListener('click',function(){setTab(i,false)});t.addEventListener('keydown',function(e){var n=null;if(e.key==='ArrowRight')n=(i+1)%3;else if(e.key==='ArrowLeft')n=(i+2)%3;else if(e.key==='Home')n=0;else if(e.key==='End')n=2;if(n!==null){e.preventDefault();setTab(n,true)}})});
panel.addEventListener('change',update);
panel.addEventListener('click',function(e){var b=e.target.closest('.bts__nf');if(!b)return;dayIdx=+b.dataset.d;var r=days.querySelectorAll('input')[dayIdx];r.checked=true;counts();setTab(+b.dataset.p,false);var first=panel.querySelector('input:not(:disabled)');if(first)first.focus()});
f.addEventListener('submit',function(e){e.preventDefault();if(go.disabled)return;var txt=sum.textContent;go.classList.add('is-busy');go.disabled=true;go.setAttribute('aria-label','Bezig met inplannen');
setTimeout(function(){go.classList.remove('is-busy');go.removeAttribute('aria-label');go.textContent='Ingepland';f.classList.add('is-done');sum.textContent='Gelukt. We bellen je '+txt.replace(/\.$/,'')+' op 010 123 4567.'},700)});
counts();var start=0;for(var i=0;i<3;i++)if(freeCount(curDay(),i)){start=i;break}setTab(start,false);
var first=panel.querySelector('input:not(:disabled)');if(first){var all=[].slice.call(panel.querySelectorAll('input:not(:disabled)'));(all[2]||first).checked=true;update()}
}
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 →