Afspraak: kalender met tijdslots New
Calendly-style appointment picker for a physio, hairdresser or installer: month calendar left (days with availability highlighted, full days grey), time slots right with taken times struck through, the chosen time splits into time plus Confirm. Times are shown in Dutch time (CET/CEST via Intl, with a notice if your clock differs). Includes loading, empty and confirmed states. Availability is mock data; in production it comes from your calendar API.
Intake fysiotherapie · 45 min · Kralingen
| ma | di | wo | do | vr | za | zo |
|---|
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.
<!-- Afspraak: kalender met tijdslots · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="bat-q"><div class="bat"><div class="bat__l"><p class="bat__svc"><b>Intake fysiotherapie</b> · 45 min · Kralingen</p><div class="bat__head"><button type="button" class="bat__nav bat__prev" aria-label="Vorige maand">‹</button><h3 class="bat__mt" id="bat-m" aria-live="polite"></h3><button type="button" class="bat__nav bat__next" aria-label="Volgende maand">›</button></div><table class="bat__g" role="grid" aria-labelledby="bat-m"><thead><tr><th scope="col">ma</th><th scope="col">di</th><th scope="col">wo</th><th scope="col">do</th><th scope="col">vr</th><th scope="col">za</th><th scope="col">zo</th></tr></thead><tbody class="bat__body"></tbody></table><p class="bat__tz"><svg width="12" height="12" viewBox="0 0 12 12" fill="none" stroke="currentColor" stroke-width="1.2" aria-hidden="true"><circle cx="6" cy="6" r="5"/><path d="M1 6h10M6 1c1.6 1.4 1.6 8.6 0 10M6 1c-1.6 1.4-1.6 8.6 0 10"/></svg><span class="bat__tzt"></span></p></div><div class="bat__r"><p class="bat__day" aria-live="polite"></p><div class="bat__slots" role="list"></div></div></div></div>
</div>/* Afspraak: kalender met tijdslots · NedDev · eigen werk. */
.bat-q{position:absolute;inset:0;container-type:inline-size}
.bat{position:absolute;inset:0;display:grid;grid-template-columns:minmax(0,205px) minmax(0,1fr);gap:14px;padding:10px 12px;background:#fff;font-family:'Switzer',system-ui,sans-serif;color:#0a0a0a;overflow:auto}
.bat__svc{margin:0 0 4px;font-size:11px;color:#5c5a56;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.bat__svc b{color:#0a0a0a}
.bat__head{display:flex;align-items:center}
.bat__mt{flex:1;margin:0;text-align:center;font-size:12.5px;font-weight:700;text-transform:capitalize}
.bat__nav{width:24px;height:22px;border:0;border-radius:4px;background:none;font-size:17px;line-height:1;cursor:pointer;color:#0a0a0a;transition:background-color .15s}
.bat__nav:hover{background:#f4f3f1}.bat__nav:focus-visible{outline:2px solid #0a0a0a}.bat__nav:disabled{color:#c0bdb7;background:none;cursor:default}
.bat__g{width:100%;border-collapse:separate;border-spacing:2px;table-layout:fixed}
.bat__g th{font:500 8.5px 'JetBrains Mono',monospace;color:#5c5a56;text-transform:uppercase;height:13px;padding:0}
.bat__d{height:21px;padding:0;text-align:center;font-size:11.5px;font-variant-numeric:tabular-nums;border-radius:50%;color:#a9a6a0;cursor:default;transition:background-color .12s,color .12s}
.bat__d.is-pad{visibility:hidden}
.bat__d.is-av{background:#fde9e1;color:#8a2a08;font-weight:600;cursor:pointer}
.bat__d.is-av:hover{background:#fbd3c3}
.bat__d.is-sel{background:#0a0a0a;color:#fff}
.bat__d:focus-visible{outline:2px solid #0a0a0a;outline-offset:1px}
.bat__d.is-today{box-shadow:inset 0 -2px 0 #F5480F}
.bat__tz{display:flex;align-items:center;gap:5px;margin:3px 0 0;font-size:10.5px;color:#5c5a56}
.bat__r{display:flex;flex-direction:column;min-height:0;border-left:1px solid rgba(10,10,10,.1);padding-left:12px}
.bat__day{margin:0 0 6px;font-size:12.5px;font-weight:700;min-height:16px}
.bat__day small{display:block;font-weight:500;font-size:10.5px;color:#5c5a56}
.bat__slots{display:flex;flex-direction:column;gap:5px;overflow:auto;max-height:190px;padding:2px 3px 2px 0;overscroll-behavior:contain}
.bat__row{display:flex;gap:5px}
.bat__s,.bat__go{flex:1;min-width:0;height:30px;border-radius:4px;font:600 12.5px 'Switzer',system-ui,sans-serif;font-variant-numeric:tabular-nums;cursor:pointer;transition:background-color .15s,color .15s,border-color .15s,flex-grow .2s}
.bat__s{background:#fff;color:#0a0a0a;border:1px solid rgba(10,10,10,.28)}
.bat__s:hover:not(:disabled){border-color:#0a0a0a;background:#f4f3f1}
.bat__s:focus-visible,.bat__go:focus-visible{outline:2px solid #0a0a0a;outline-offset:2px}
.bat__s:disabled{border-style:dashed;color:#8a8781;text-decoration:line-through;cursor:not-allowed;background:#fafaf9}
.bat__s[aria-pressed="true"]{background:#5c5a56;border-color:#5c5a56;color:#fff}
.bat__go{background:#0a0a0a;color:#fff;border:1px solid #0a0a0a}
.bat__go:hover{background:#F5480F;border-color:#F5480F;color:#0a0a0a}
.bat__go:active{transform:translateY(1px)}
.bat__sk{height:30px;border-radius:4px;background:linear-gradient(90deg,#f1efec 0,#e7e4e0 50%,#f1efec 100%) 0 0/200% 100%;animation:bat-sh 1s linear infinite}
@keyframes bat-sh{to{background-position:-200% 0}}
.bat__empty{margin:0;padding:12px;border:1px dashed rgba(10,10,10,.25);border-radius:4px;font-size:12px;line-height:1.4;color:#5c5a56}
.bat__done{padding:10px;border-radius:4px;background:#e8f3ec;color:#14532d;font-size:12px;line-height:1.45}
.bat__done b{display:block;font-size:13px}
.bat__done button{margin-top:6px;border:0;background:none;padding:0;font:600 12px 'Switzer',system-ui,sans-serif;color:#14532d;text-decoration:underline;cursor:pointer}
@container (max-width:400px){.bat{grid-template-columns:1fr}.bat__r{border-left:0;padding-left:0;border-top:1px solid rgba(10,10,10,.1);padding-top:8px}.bat__slots{max-height:none}}
@media (prefers-reduced-motion:reduce){.bat *{transition:none!important;animation:none!important}}// Afspraak: kalender met tijdslots · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.bat');if(!w)return;
var body=w.querySelector('.bat__body'),mt=w.querySelector('.bat__mt'),day=w.querySelector('.bat__day'),slots=w.querySelector('.bat__slots'),prev=w.querySelector('.bat__prev'),next=w.querySelector('.bat__next'),tzt=w.querySelector('.bat__tzt');
var DAGL=['zondag','maandag','dinsdag','woensdag','donderdag','vrijdag','zaterdag'],MND=['januari','februari','maart','april','mei','juni','juli','augustus','september','oktober','november','december'];
var TIMES=['08:00','08:45','09:30','10:15','11:00','11:45','13:00','13:45','14:30','15:15','16:00','16:45'];
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 same(a,b){return !!a&&!!b&&a.getTime()===b.getTime()}
function seed(d){var n=d.getFullYear()*372+d.getMonth()*31+d.getDate();return function(){n=(n*9301+49297)%233280;return n/233280}}
/* nep-beschikbaarheid: in productie uit je agenda (bv. via een eigen API) */
function avail(d){if(d<T||d.getDay()===0)return [];var r=seed(d),list=d.getDay()===6?TIMES.slice(0,6):TIMES;if(r()<.18)return list.map(function(t){return {t:t,busy:true}});
var now=new Date();return list.map(function(t){var busy=r()<.4;if(same(d,T)){var p=t.split(':');if(+p[0]*60+ +p[1]<=now.getHours()*60+now.getMinutes()+60)busy=true}return {t:t,busy:busy}})}
function free(d){return avail(d).some(function(s){return !s.busy})}
function tz(d){try{var p=new Intl.DateTimeFormat('en-GB',{timeZone:'Europe/Amsterdam',timeZoneName:'short'}).formatToParts(d);for(var i=0;i<p.length;i++)if(p[i].type==='timeZoneName')return p[i].value}catch(e){}return 'NL'}
var mine='';try{mine=Intl.DateTimeFormat().resolvedOptions().timeZone}catch(e){}
tzt.textContent='Nederlandse tijd ('+tz(new Date())+')'+(mine&&mine!=='Europe/Amsterdam'&&mine!=='Europe/Brussels'?', jij zit op '+mine:'');
var sel=null,foc=null,view,timer;
for(var i=0;i<40&&!foc;i++){var c=add(T,i);if(free(c))foc=c}foc=foc||new Date(T);view=new Date(foc.getFullYear(),foc.getMonth(),1);
function render(){mt.textContent=MND[view.getMonth()]+' '+view.getFullYear();var s=add(view,-((view.getDay()+6)%7)),h='';
for(var r=0;r<6;r++){h+='<tr>';for(var c=0;c<7;c++){var d=add(s,r*7+c);if(d.getMonth()!==view.getMonth()){h+='<td class="bat__d is-pad" aria-hidden="true"></td>';continue}var av=free(d);
h+='<td role="gridcell" class="bat__d'+(av?' is-av':'')+(same(d,sel)?' is-sel':'')+(same(d,T)?' is-today':'')+'" data-t="'+d.getTime()+'" tabindex="'+(same(d,foc)?0:-1)+'" aria-selected="'+(same(d,sel)?'true':'false')+'"'+(av?'':' aria-disabled="true"')+' aria-label="'+DAGL[d.getDay()]+' '+d.getDate()+' '+MND[d.getMonth()]+(av?', plek vrij':', geen plek')+'">'+d.getDate()+'</td>'}h+='</tr>'}
body.innerHTML=h;prev.disabled=view.getFullYear()*12+view.getMonth()<=T.getFullYear()*12+T.getMonth()}
function showDay(d){sel=d;clearTimeout(timer);var lab=DAGL[d.getDay()]+' '+d.getDate()+' '+MND[d.getMonth()];
if(!free(d)){day.innerHTML=lab+'<small>Geen tijden vrij</small>';slots.innerHTML='<p class="bat__empty">Deze dag is vol. De eerstvolgende dag met plek is gemarkeerd in de kalender.</p>';return}
day.innerHTML=lab+'<small>Tijden laden</small>';slots.setAttribute('aria-busy','true');slots.innerHTML='<div class="bat__sk"></div><div class="bat__sk"></div><div class="bat__sk"></div><div class="bat__sk"></div>';
timer=setTimeout(function(){var a=avail(d),n=a.filter(function(s){return !s.busy}).length;day.innerHTML=lab+'<small>'+n+' tijden vrij</small>';slots.removeAttribute('aria-busy');
slots.innerHTML=a.map(function(s){return '<div class="bat__row" role="listitem"><button type="button" class="bat__s" aria-pressed="false"'+(s.busy?' disabled aria-label="'+s.t+', bezet"':' aria-label="'+s.t+', vrij"')+'>'+s.t+'</button></div>'}).join('')},380)}
function focusCell(){var c=body.querySelector('[tabindex="0"]');if(c)c.focus()}
function move(d){if(d<new Date(T.getFullYear(),T.getMonth(),1))return;foc=d;if(d.getMonth()!==view.getMonth()||d.getFullYear()!==view.getFullYear())view=new Date(d.getFullYear(),d.getMonth(),1);render();focusCell()}
function pickDay(d){foc=d;if(!free(d)){showDay(d);render();focusCell();return}showDay(d);render();focusCell()}
body.addEventListener('click',function(e){var c=e.target.closest('td[data-t]');if(c)pickDay(new Date(+c.dataset.t))});
body.addEventListener('keydown',function(e){var k=e.key,d=new Date(foc),dow=(d.getDay()+6)%7;
if(k==='ArrowLeft')d=add(d,-1);else if(k==='ArrowRight')d=add(d,1);else if(k==='ArrowUp')d=add(d,-7);else if(k==='ArrowDown')d=add(d,7);else if(k==='Home')d=add(d,-dow);else if(k==='End')d=add(d,6-dow);
else if(k==='PageUp')d=new Date(d.getFullYear(),d.getMonth()-1,Math.min(d.getDate(),28));else if(k==='PageDown')d=new Date(d.getFullYear(),d.getMonth()+1,Math.min(d.getDate(),28));
else if(k==='Enter'||k===' '){e.preventDefault();pickDay(foc);return}else return;e.preventDefault();move(d)});
prev.addEventListener('click',function(){view=new Date(view.getFullYear(),view.getMonth()-1,1);render()});
next.addEventListener('click',function(){view=new Date(view.getFullYear(),view.getMonth()+1,1);render()});
slots.addEventListener('click',function(e){var go=e.target.closest('.bat__go');
if(go){var t=go.dataset.t;day.innerHTML='Afspraak staat<small>'+DAGL[sel.getDay()]+' '+sel.getDate()+' '+MND[sel.getMonth()]+'</small>';slots.innerHTML='<div class="bat__done" role="status"><b>'+t+' tot '+go.dataset.e+'</b>Intake bij Lotte Bakker, Oudedijk 112. Je krijgt een bevestiging per mail.<br><button type="button" class="bat__redo">Andere tijd kiezen</button></div>';slots.querySelector('.bat__redo').focus();return}
if(e.target.closest('.bat__redo')){showDay(sel);return}
var b=e.target.closest('.bat__s');if(!b||b.disabled)return;
slots.querySelectorAll('.bat__go').forEach(function(g){g.remove()});slots.querySelectorAll('.bat__s').forEach(function(x){x.setAttribute('aria-pressed','false')});
b.setAttribute('aria-pressed','true');var p=b.textContent.split(':'),m=+p[0]*60+ +p[1]+45,e2=(m/60|0)+':'+('0'+m%60).slice(-2);if(e2.length<5)e2='0'+e2;
var g=document.createElement('button');g.type='button';g.className='bat__go';g.textContent='Bevestig';g.dataset.t=b.textContent;g.dataset.e=e2;g.setAttribute('aria-label','Bevestig '+b.textContent+' tot '+e2);b.parentNode.appendChild(g);g.focus()});
render();showDay(foc);
}
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 →