Afspraak: kalender met tijdslots Nieuw
Calendly-achtige afsprakenplanner voor een fysio, kapper of installateur: maandkalender links (dagen met plek gemarkeerd, volle dagen grijs), tijdslots rechts met bezette tijden doorgestreept, gekozen tijd splitst in tijd plus Bevestig. Tijden staan in Nederlandse tijd (CET/CEST via Intl, met melding als jouw klok afwijkt). Laadstaat, lege staat en bevestiging zitten erin. Beschikbaarheid is nep-data; in productie uit je agenda-API.
Intake fysiotherapie · 45 min · Kralingen
| ma | di | wo | do | vr | za | zo |
|---|
Zo gebruik je het
- Plak de HTML op de plek waar het onderdeel moet komen.
- Zet de CSS in je stylesheet. Alle klassen hebben een eigen voorvoegsel, dus niets botst met je eigen opmaak.
- Voeg de JavaScript toe en roep init() aan met het element waar de HTML in staat.
<!-- 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'));
Meer uit formulieren & invoer
Iets gezien dat je mooi vindt?
Zet het in je selectie en stuur hem op. Dan bouwen we je site eromheen, in jouw huisstijl.
Neem contact op →