Weekagenda met dagweergave New
Physio practice calendar in week and day view: appointments as blocks on a time axis, a now line that moves every minute, previous/next week, back to today and a detail card when you click a block (cancel works). CSS grid with positions calculated from minutes; on narrow screens it starts in day view. In production you connect it to your calendar API or an iCal feed.
How to use it
- Paste the HTML where the numbers & charts 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.
<!-- Weekagenda met dagweergave · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="kwk">
<div class="kwk__head">
<button type="button" class="kwk__today">Vandaag</button>
<button type="button" class="kwk__nav" data-d="-1" aria-label="Vorige week"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg></button>
<button type="button" class="kwk__nav" data-d="1" aria-label="Volgende week"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 6 6 6-6 6"/></svg></button>
<h3 class="kwk__title" aria-live="polite"></h3>
<div class="kwk__seg" role="group" aria-label="Weergave">
<button type="button" data-v="week" aria-pressed="true">Week</button><button type="button" data-v="dag" aria-pressed="false">Dag</button>
</div>
</div>
<div class="kwk__days"></div>
<div class="kwk__scroll" tabindex="0" aria-label="Afspraken">
<div class="kwk__grid"></div>
</div>
<div class="kwk__card" role="dialog" aria-labelledby="kwk-ct" hidden>
<div class="kwk__ctop"><h4 class="kwk__ct" id="kwk-ct" tabindex="-1"></h4><button type="button" class="kwk__close" aria-label="Details sluiten"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>
<p class="kwk__cm"></p>
<div class="kwk__cact"><button type="button" class="kwk__cancel">Afspraak annuleren</button></div>
</div>
<p class="kwk__sr" aria-live="polite"></p>
</div>
</div>/* Weekagenda met dagweergave · NedDev · eigen werk. */
.kwk{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.1);--hh:52px;position:absolute;inset:0;display:flex;flex-direction:column;font-family:'Switzer',system-ui,sans-serif;font-size:12px;line-height:1.3;color:var(--ink);background:#fff;container:kwk/inline-size;text-align:left}
.kwk *,.kwk *::before,.kwk *::after{box-sizing:border-box}
.kwk svg{width:14px;height:14px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.kwk__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);margin:0}
.kwk__head{display:flex;align-items:center;gap:4px;padding:8px 10px;border-bottom:1px solid var(--line)}
.kwk__today,.kwk__nav,.kwk__seg button{height:26px;border:1px solid rgba(10,10,10,.2);border-radius:4px;background:#fff;color:var(--ink);font:500 11.5px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:border-color .15s,background-color .15s}
.kwk__today{padding:0 8px}
.kwk__nav{width:26px;display:grid;place-items:center}
.kwk__today:hover,.kwk__nav:hover,.kwk__seg button:hover{border-color:var(--ink)}
.kwk__title{flex:1;min-width:0;margin:0 0 0 4px;font-size:13px;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kwk__seg{display:flex}
.kwk__seg button{padding:0 9px}
.kwk__seg button:first-child{border-radius:4px 0 0 4px}
.kwk__seg button:last-child{border-radius:0 4px 4px 0;margin-left:-1px}
.kwk__seg button[aria-pressed="true"]{background:var(--ink);color:#fff;border-color:var(--ink)}
.kwk button:focus-visible,.kwk__scroll:focus-visible{outline:2px solid var(--pop);outline-offset:1px}
.kwk__days{display:grid;grid-template-columns:34px repeat(var(--n,5),1fr);border-bottom:1px solid var(--line)}
.kwk__dh{padding:5px 4px;font-size:11px;color:var(--mut);text-align:center;border-left:1px solid var(--line);white-space:nowrap}
.kwk__dh b{font-weight:600;color:var(--ink);font-size:12px}
.kwk__dh.is-today b{color:var(--pd)}
.kwk__dh button{width:100%;min-height:24px;border:0;background:none;font:inherit;color:inherit;cursor:pointer;padding:0;border-radius:4px}
.kwk__dh button[aria-pressed="true"]{background:var(--ink);color:#fff}
.kwk__dh button[aria-pressed="true"] b{color:#fff}
.kwk__scroll{flex:1;min-height:0;overflow:auto;scrollbar-width:thin}
.kwk__grid{position:relative;display:grid;grid-template-columns:34px repeat(var(--n,5),1fr);height:calc(var(--hh) * 12)}
.kwk__hours{position:relative}
.kwk__hours span{position:absolute;right:5px;font-size:11px;color:var(--mut);transform:translateY(-50%);font-variant-numeric:tabular-nums}
.kwk__col{position:relative;border-left:1px solid var(--line);background:repeating-linear-gradient(to bottom,transparent 0 calc(var(--hh) - 1px),var(--line) calc(var(--hh) - 1px) var(--hh))}
.kwk__col.is-today{background-color:#fcf8f6}
.kwk__ev{position:absolute;left:2px;right:2px;display:block;padding:2px 4px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 11px/1.2 'Switzer',system-ui,sans-serif;text-align:left;overflow:hidden;cursor:pointer;transition:background-color .15s,transform .15s}
.kwk__ev:hover{background:#f4f3f1}
.kwk__ev b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kwk__ev span{display:block;color:var(--mut);white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.kwk__ev--intake{background:var(--pop);border-color:var(--pop)}
.kwk__ev--intake:hover{background:#e3400a}
.kwk__ev--intake span{color:var(--ink)}
.kwk__ev--groep{background:var(--ink);color:#fff}
.kwk__ev--groep:hover{background:#2a2a2a}
.kwk__ev--groep span{color:#d8d6d2}
.kwk__ev.is-cancel{background:repeating-linear-gradient(135deg,#fff 0 4px,#efedea 4px 8px);border-style:dashed;border-color:#8a8a8a;color:var(--mut)}
.kwk__ev.is-cancel b{text-decoration:line-through}
.kwk__ev.is-cancel span{color:var(--mut)}
.kwk__ev[aria-expanded="true"]{outline:2px solid var(--ink);outline-offset:1px}
.kwk__now{position:absolute;left:-1px;right:0;height:0;border-top:2px solid var(--pop);z-index:2;pointer-events:none}
.kwk__now::before{content:'';position:absolute;left:-4px;top:-5px;width:8px;height:8px;border-radius:4px;background:var(--pop)}
.kwk__card{position:absolute;left:10px;right:10px;bottom:10px;z-index:5;padding:10px 12px;background:#fff;border:1px solid var(--ink);border-radius:4px;max-width:340px;margin-left:auto}
.kwk__card[hidden]{display:none}
.kwk__ctop{display:flex;align-items:flex-start;gap:8px}
.kwk__ct{flex:1;margin:0;font-size:13px;font-weight:600}
.kwk__ct:focus{outline:none}
.kwk__close{width:24px;height:24px;display:grid;place-items:center;border:0;border-radius:4px;background:none;cursor:pointer;color:var(--ink)}
.kwk__close:hover{background:#f4f3f1}
.kwk__cm{margin:4px 0 8px;color:#333;font-size:11.5px;line-height:1.45}
.kwk__cancel{height:26px;padding:0 10px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 11.5px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s,color .15s}
.kwk__cancel:hover{background:var(--ink);color:#fff}
.kwk__cancel:disabled{opacity:.4;cursor:not-allowed}
.kwk__cancel:disabled:hover{background:#fff;color:var(--ink)}
@container kwk (max-width:380px){.kwk__today{display:none}.kwk__title{font-size:12px}}
@media (prefers-reduced-motion:reduce){.kwk *{transition:none!important}}// Weekagenda met dagweergave · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.kwk'); if (!w) return;
const grid = w.querySelector('.kwk__grid'), days = w.querySelector('.kwk__days'), sc = w.querySelector('.kwk__scroll');
const title = w.querySelector('.kwk__title'), card = w.querySelector('.kwk__card'), sr = w.querySelector('.kwk__sr');
const START = 7, END = 19, HH = 52;
const fmtD = new Intl.DateTimeFormat('nl-NL', { weekday: 'short', day: 'numeric' });
const fmtM = new Intl.DateTimeFormat('nl-NL', { day: 'numeric', month: 'short' });
const fmtL = new Intl.DateTimeFormat('nl-NL', { weekday: 'long', day: 'numeric', month: 'long' });
const today = new Date(); today.setHours(0, 0, 0, 0);
const monday = d => { const x = new Date(d); x.setHours(0, 0, 0, 0); x.setDate(x.getDate() - ((x.getDay() + 6) % 7)); return x; };
let wk = monday(today), view = w.clientWidth < 380 ? 'dag' : 'week';
let dayIdx = Math.min(4, (today.getDay() + 6) % 7);
const cancelled = new Set();
// Nep-agenda: vijf dagpatronen die per week verschuiven.
const P = [
[['08:00', 30, 'Sanne de Vries', 'beh'], ['09:00', 60, 'Ahmed El Idrissi', 'intake'], ['10:30', 30, 'Joost Bakker', 'beh'], ['13:00', 60, 'Groepstraining rug', 'groep'], ['15:00', 30, 'Fatma Yilmaz', 'beh'], ['16:30', 45, 'Kees Verbeek', 'beh']],
[['08:30', 45, 'Lotte Janssen', 'beh'], ['10:00', 30, 'Rohan Mahabier', 'beh'], ['11:00', 60, 'Ilse de Wit', 'intake'], ['14:00', 30, 'Mark Smit', 'beh'], ['17:00', 60, 'Groepstraining nek', 'groep']],
[['08:00', 60, 'Groepstraining ochtend', 'groep'], ['09:30', 30, 'Noah Visser', 'beh'], ['11:30', 30, 'Emma Mulder', 'beh'], ['13:30', 60, 'Yusuf Kaya', 'intake'], ['15:30', 45, 'Anouk Bos', 'beh']],
[['09:00', 30, 'Daan Meijer', 'beh'], ['10:00', 60, 'Sofia Rossi', 'intake'], ['12:00', 30, 'Pieter Kok', 'beh'], ['14:30', 30, 'Hanna Vos', 'beh'], ['16:00', 60, 'Groepstraining rug', 'groep']],
[['08:30', 30, 'Mila Peters', 'beh'], ['09:30', 45, 'Thomas Hendriks', 'beh'], ['11:00', 30, 'Lisa van Dijk', 'beh'], ['13:00', 60, 'Bram de Graaf', 'intake'], ['15:00', 30, 'Eva Dekker', 'beh']]
];
const TYPE = { beh: 'Behandeling', intake: 'Intake', groep: 'Groepstraining' };
const WHO = ['Marloes (fysio)', 'Jeroen (fysio)', 'Samira (manueel)'];
const wkNo = d => Math.round((monday(d) - monday(new Date(2026, 0, 5))) / 6048e5);
function events(date) {
const di = (date.getDay() + 6) % 7, n = wkNo(date);
return P[(di + n * 2 + 50) % 5].map((e, i) => {
const [h, m] = e[0].split(':').map(Number);
return { id: date.toISOString().slice(0, 10) + '-' + i, start: h * 60 + m, dur: e[1], who: e[2], type: e[3], by: WHO[(i + di + n) % 3], room: 'Ruimte ' + ((i + di) % 3 + 1), date };
});
}
const hm = m => Math.floor(m / 60) + ':' + String(m % 60).padStart(2, '0');
const same = (a, b) => a.getTime() === b.getTime();
function shown() {
const out = [];
if (view === 'week') for (let i = 0; i < 5; i++) { const d = new Date(wk); d.setDate(d.getDate() + i); out.push(d); }
else { const d = new Date(wk); d.setDate(d.getDate() + dayIdx); out.push(d); }
return out;
}
function render() {
closeCard(false);
const ds = shown();
w.style.setProperty('--n', ds.length);
const fri = new Date(wk); fri.setDate(fri.getDate() + 4);
title.textContent = view === 'week' ? (wk.getMonth() === fri.getMonth() ? wk.getDate() : fmtM.format(wk)) + ' t/m ' + fmtM.format(fri) + ' ' + fri.getFullYear() : fmtL.format(ds[0]);
w.querySelectorAll('.kwk__seg button').forEach(b => b.setAttribute('aria-pressed', String(b.dataset.v === view)));
w.querySelector('[data-d="-1"]').setAttribute('aria-label', view === 'week' ? 'Vorige week' : 'Vorige dag');
w.querySelector('[data-d="1"]').setAttribute('aria-label', view === 'week' ? 'Volgende week' : 'Volgende dag');
// kopregel: in dagweergave zijn de dagen knoppen
let h = '<div></div>';
for (let i = 0; i < 5; i++) {
const d = new Date(wk); d.setDate(d.getDate() + i);
const p = fmtD.formatToParts(d), wd = p.find(x => x.type === 'weekday').value, dn = p.find(x => x.type === 'day').value;
const t = same(d, today) ? ' is-today' : '';
if (view === 'week') h += '<div class="kwk__dh' + t + '">' + wd + ' <b>' + dn + '</b></div>';
else if (i === 0) h += '<div class="kwk__dh" style="display:flex;gap:2px;border-left:0">' +
[0, 1, 2, 3, 4].map(j => { const e = new Date(wk); e.setDate(e.getDate() + j); const q = fmtD.formatToParts(e); return '<button type="button" data-day="' + j + '" aria-pressed="' + (j === dayIdx) + '" aria-label="' + fmtL.format(e) + '">' + q[0].value + ' <b>' + q[2].value + '</b></button>'; }).join('') + '</div>';
}
days.innerHTML = h;
days.style.gridTemplateColumns = view === 'week' ? '' : '34px 1fr';
let g = '<div class="kwk__hours">';
for (let i = START + 1; i < END; i++) g += '<span style="top:' + (i - START) * HH + 'px">' + i + ':00</span>';
g += '</div>';
ds.forEach(d => {
g += '<div class="kwk__col' + (same(d, today) ? ' is-today' : '') + '" data-date="' + d.getTime() + '">';
events(d).forEach(e => {
const top = (e.start - START * 60) / 60 * HH, hgt = e.dur / 60 * HH - 2;
const c = cancelled.has(e.id);
g += '<button type="button" class="kwk__ev kwk__ev--' + e.type + (c ? ' is-cancel' : '') + '" data-id="' + e.id + '" aria-haspopup="dialog" aria-expanded="false" style="top:' + top + 'px;height:' + hgt + 'px" aria-label="' + TYPE[e.type] + ', ' + e.who + ', ' + fmtL.format(d) + ' ' + hm(e.start) + ' tot ' + hm(e.start + e.dur) + (c ? ', geannuleerd' : '') + '">' +
'<b>' + e.who + '</b>' + (hgt > 30 ? '<span>' + hm(e.start) + ' · ' + TYPE[e.type] + '</span>' : '') + '</button>';
});
g += '</div>';
});
grid.innerHTML = g;
nowLine();
}
function nowLine() {
grid.querySelectorAll('.kwk__now').forEach(n => n.remove());
const now = new Date(), mins = now.getHours() * 60 + now.getMinutes();
if (mins < START * 60 || mins > END * 60) return;
const col = grid.querySelector('.kwk__col.is-today'); if (!col) return;
const n = document.createElement('div'); n.className = 'kwk__now'; n.setAttribute('aria-hidden', 'true');
n.style.top = (mins - START * 60) / 60 * HH + 'px'; col.appendChild(n);
}
let cur = null;
function find(id) { const d = new Date(+grid.querySelector('[data-id="' + id + '"]').closest('.kwk__col').dataset.date); return events(d).find(e => e.id === id); }
function openCard(btn) {
const e = find(btn.dataset.id); cur = { e, btn };
grid.querySelectorAll('.kwk__ev').forEach(b => b.setAttribute('aria-expanded', String(b === btn)));
const c = cancelled.has(e.id);
card.querySelector('.kwk__ct').textContent = TYPE[e.type] + (e.type === 'groep' ? '' : ': ' + e.who) + (c ? ' (geannuleerd)' : '');
card.querySelector('.kwk__cm').textContent = fmtL.format(e.date) + ', ' + hm(e.start) + ' tot ' + hm(e.start + e.dur) + ' · ' + e.by + ' · ' + e.room + (e.type === 'groep' ? ' · 6 van 8 plekken bezet' : '');
card.querySelector('.kwk__cancel').disabled = c;
card.hidden = false; card.querySelector('.kwk__ct').focus();
}
function closeCard(focus) {
if (card.hidden) return;
card.hidden = true;
grid.querySelectorAll('.kwk__ev').forEach(b => b.setAttribute('aria-expanded', 'false'));
if (focus !== false && cur) { const b = grid.querySelector('[data-id="' + cur.e.id + '"]'); if (b) b.focus(); }
}
grid.addEventListener('click', e => { const b = e.target.closest('.kwk__ev'); if (b) openCard(b); });
card.querySelector('.kwk__close').addEventListener('click', () => closeCard());
card.querySelector('.kwk__cancel').addEventListener('click', () => {
cancelled.add(cur.e.id); const id = cur.e.id, t = hm(cur.e.start);
render(); const b = grid.querySelector('[data-id="' + id + '"]'); if (b) b.focus();
sr.textContent = 'Afspraak van ' + t + ' geannuleerd';
});
w.addEventListener('keydown', e => { if (e.key === 'Escape' && !card.hidden) { e.preventDefault(); closeCard(); } });
w.querySelectorAll('.kwk__nav').forEach(b => b.addEventListener('click', () => {
const d = +b.dataset.d;
if (view === 'week') wk.setDate(wk.getDate() + 7 * d);
else { dayIdx += d; if (dayIdx < 0) { dayIdx = 4; wk.setDate(wk.getDate() - 7); } if (dayIdx > 4) { dayIdx = 0; wk.setDate(wk.getDate() + 7); } }
render();
}));
w.querySelector('.kwk__today').addEventListener('click', () => { wk = monday(today); dayIdx = Math.min(4, (today.getDay() + 6) % 7); render(); scrollNow(); });
w.querySelector('.kwk__seg').addEventListener('click', e => { const b = e.target.closest('button'); if (!b || b.dataset.v === view) return; view = b.dataset.v; render(); b.focus(); });
days.addEventListener('click', e => { const b = e.target.closest('[data-day]'); if (!b) return; dayIdx = +b.dataset.day; render(); const n = days.querySelector('[data-day="' + dayIdx + '"]'); if (n) n.focus(); });
function scrollNow() { const h = new Date().getHours(); sc.scrollTop = Math.max(0, (Math.min(Math.max(h, 8), 16) - START - 1) * HH); }
render(); scrollNow();
setInterval(nowLine, 60000);
}
init(document.querySelector('.jouw-container'));
More numbers & charts
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 →