Contact per onderwerp New
Contact block that first asks what it is about and then shows only the channels that fit (call, WhatsApp, form, appointment), each with an honest response time and whether the phone line is open right now. The form and the appointment choice work inline with error and success states. Focus follows each step, back returns to the chosen topic. In production you connect opening hours and slots to your planning.
Waar gaat je vraag over?
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.
<!-- Contact per onderwerp · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="cpo">
<div class="cpo__step cpo__topics">
<h3 class="cpo__h" tabindex="-1">Waar gaat je vraag over?</h3>
<ul class="cpo__list" role="list"></ul>
</div>
<div class="cpo__step cpo__chan" hidden>
<div class="cpo__top"><button type="button" class="cpo__back"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg>Ander onderwerp</button></div>
<h3 class="cpo__h cpo__ch" tabindex="-1"></h3>
<ul class="cpo__ways" role="list"></ul>
<div class="cpo__extra"></div>
</div>
<p class="cpo__sr" aria-live="polite"></p>
</div>
</div>/* Contact per onderwerp · NedDev · eigen werk. */
.cpo{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.12);--ok:#1d6b3a;position:absolute;inset:0;overflow:auto;padding:12px 14px;font-family:'Switzer',system-ui,sans-serif;font-size:12.5px;line-height:1.4;color:var(--ink);background:#f4f3f1;scrollbar-width:thin;text-align:left}
.cpo *,.cpo *::before,.cpo *::after{box-sizing:border-box}
.cpo svg{width:16px;height:16px;fill:none;stroke:currentColor;stroke-width:1.7;stroke-linecap:round;stroke-linejoin:round;flex:none}
.cpo__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);margin:0}
.cpo__step[hidden]{display:none}
.cpo__h{margin:0 0 8px;font-size:15px;font-weight:600;letter-spacing:-.01em}
.cpo__h:focus{outline:none}
.cpo__list,.cpo__ways{list-style:none;margin:0;padding:0;display:grid;gap:4px}
.cpo__topic{display:flex;align-items:center;gap:10px;width:100%;padding:8px 10px;border:1px solid var(--line);border-radius:4px;background:#fff;color:var(--ink);font:inherit;text-align:left;cursor:pointer;transition:border-color .15s,background-color .15s}
.cpo__topic:hover{border-color:var(--ink)}
.cpo__topic b{display:block;font-weight:600;font-size:13px;color:var(--ink)}
.cpo__topic span{display:block;color:var(--mut);font-size:11.5px}
.cpo__topic > svg:last-child{margin-left:auto;color:var(--mut)}
.cpo__topic--spoed b{color:var(--pd)}
.cpo__top{margin-bottom:6px}
.cpo__back{display:inline-flex;align-items:center;gap:2px;padding:2px 4px 2px 0;border:0;background:none;color:var(--ink);font:500 12px/1.2 'Switzer',system-ui,sans-serif;cursor:pointer;text-decoration:underline;text-underline-offset:2px}
.cpo__back:hover{color:var(--pd)}
.cpo__way{display:flex;align-items:center;gap:10px;padding:8px 10px;background:#fff;border:1px solid var(--line);border-radius:4px}
.cpo__way.is-first{border-color:var(--ink)}
.cpo__ico{display:grid;place-items:center;width:30px;height:30px;border-radius:4px;background:#f4f3f1}
.cpo__way.is-first .cpo__ico{background:var(--pop)}
.cpo__txt{flex:1;min-width:0}
.cpo__txt b{display:block;font-size:13px;font-weight:600}
.cpo__txt span{display:block;font-size:11.5px;color:var(--mut)}
.cpo__txt em{font-style:normal;font-weight:600}
.cpo__txt .is-open{color:var(--ok)}
.cpo__go{flex:none;display:inline-flex;align-items:center;height:28px;padding:0 10px;border:1px solid var(--ink);border-radius:4px;background:#fff;color:var(--ink);font:500 12px/1 'Switzer',system-ui,sans-serif;text-decoration:none;cursor:pointer;white-space:nowrap;transition:background-color .15s,color .15s}
.cpo__go:hover{background:var(--ink);color:#fff}
.cpo__way.is-first .cpo__go{background:var(--ink);color:#fff}
.cpo__way.is-first .cpo__go:hover{background:#2a2a2a}
.cpo__go[aria-expanded="true"]{background:var(--ink);color:#fff}
.cpo button:focus-visible,.cpo a:focus-visible,.cpo textarea:focus-visible,.cpo input:focus-visible{outline:2px solid var(--pop);outline-offset:2px}
.cpo__extra:empty{display:none}
.cpo__extra{margin-top:8px;padding:10px;background:#fff;border:1px solid var(--line);border-radius:4px}
.cpo__lbl{display:block;font-weight:600;font-size:12px;margin:0 0 3px}
.cpo__in{display:block;width:100%;padding:7px 8px;border:1px solid rgba(10,10,10,.3);border-radius:4px;background:#fff;font:500 12.5px/1.35 'Switzer',system-ui,sans-serif;color:var(--ink);margin-bottom:8px}
.cpo__in[aria-invalid="true"]{border-color:var(--pd)}
textarea.cpo__in{resize:vertical;min-height:52px}
@supports (field-sizing:content){textarea.cpo__in{field-sizing:content;min-height:52px;max-height:120px}}
.cpo__err{margin:-5px 0 8px;color:var(--pd);font-size:11.5px;font-weight:500}
.cpo__err:empty{display:none}
.cpo__send{height:30px;padding:0 12px;border:0;border-radius:4px;background:var(--ink);color:#fff;font:600 12px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s}
.cpo__send:hover{background:#2a2a2a}
.cpo__send[aria-busy="true"]{opacity:.6;cursor:progress}
.cpo__slots{display:flex;flex-wrap:wrap;gap:4px;margin-bottom:8px}
.cpo__slot{padding:6px 8px;border:1px solid rgba(10,10,10,.3);border-radius:4px;background:#fff;font:500 12px/1.2 'Switzer',system-ui,sans-serif;color:var(--ink);cursor:pointer;transition:border-color .15s}
.cpo__slot:hover{border-color:var(--ink)}
.cpo__slot[aria-checked="true"]{background:var(--ink);color:#fff;border-color:var(--ink)}
.cpo__ok{margin:0;display:flex;gap:8px;align-items:flex-start;color:var(--ink)}
.cpo__ok svg{color:var(--ok);margin-top:1px}
@media (prefers-reduced-motion:reduce){.cpo *{transition:none!important}}// Contact per onderwerp · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.cpo'); if (!w) return;
const sTop = w.querySelector('.cpo__topics'), sCh = w.querySelector('.cpo__chan');
const list = w.querySelector('.cpo__list'), ways = w.querySelector('.cpo__ways'), extra = w.querySelector('.cpo__extra');
const ch = w.querySelector('.cpo__ch'), sr = w.querySelector('.cpo__sr');
const I = {
tel: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M5 4h3l2 5-2.5 1.5a11 11 0 0 0 5 5L14 13l5 2v3a2 2 0 0 1-2 2A16 16 0 0 1 3 6a2 2 0 0 1 2-2"/></svg>',
wa: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="M4 20l1.3-4A8 8 0 1 1 8 18.7z"/><path d="M9 9.5c.5 2 2.5 4 4.5 4.5l1-1.2 2 .8"/></svg>',
form: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="m3 7 9 6 9-6"/></svg>',
cal: '<svg viewBox="0 0 24 24" aria-hidden="true"><rect x="4" y="5" width="16" height="15" rx="2"/><path d="M4 10h16M9 3v4M15 3v4"/></svg>',
chev: '<svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 6 6 6-6 6"/></svg>',
ok: '<svg viewBox="0 0 24 24" aria-hidden="true"><circle cx="12" cy="12" r="9"/><path d="m8 12 3 3 5-6"/></svg>'
};
const now = new Date(), d = now.getDay(), h = now.getHours() + now.getMinutes() / 60;
const kantoor = d >= 1 && d <= 5 && h >= 9 && h < 17;
const telKantoor = kantoor ? '<em class="is-open">Nu bereikbaar</em>, ma-vr 9:00-17:00' : 'Gesloten, ma-vr 9:00-17:00';
const W = {
spoed: { k: 'tel', t: 'Bel de storingslijn', r: '<em class="is-open">Direct</em>, 24 uur per dag', a: 'tel:+31101234567', l: '010 123 4567' },
tel: { k: 'tel', t: 'Bel het klantcontactcentrum', r: telKantoor, a: 'tel:+31101234560', l: '010 123 4560' },
wa: { k: 'wa', t: 'WhatsApp', r: 'Antwoord binnen 2 uur op werkdagen', a: 'https://wa.me/31612345678', l: 'Stuur bericht' },
form: { k: 'form', t: 'Formulier', r: 'Antwoord binnen 3 werkdagen per mail', l: 'Schrijf ons' },
cal: { k: 'cal', t: 'Afspraak met een monteur', r: 'Kies zelf een dagdeel, binnen 5 werkdagen', l: 'Kies moment' }
};
const T = [
{ id: 'spoed', t: 'Reparatie met spoed', s: 'Lekkage, geen verwarming of stroom', w: ['spoed'], note: 'Ruik je gas? Ga naar buiten en bel de storingsdienst van je netbeheerder.' },
{ id: 'rep', t: 'Reparatie zonder spoed', s: 'Klemmende deur, kraan, ventilatie', w: ['cal', 'wa', 'tel'] },
{ id: 'huur', t: 'Huur en betalen', s: 'Betalingsregeling, huurverhoging', w: ['tel', 'wa', 'form'] },
{ id: 'overlast', t: 'Overlast melden', s: 'Geluid, afval, buren', w: ['form', 'tel'] },
{ id: 'anders', t: 'Iets anders', s: 'Verhuizen, woning ruilen, klachten', w: ['form', 'wa'] }
];
const esc = s => String(s).replace(/[&<>"]/g, c => ({ '&': '&', '<': '<', '>': '>', '"': '"' })[c]);
list.innerHTML = T.map(t => '<li><button type="button" class="cpo__topic' + (t.id === 'spoed' ? ' cpo__topic--spoed' : '') + '" data-id="' + t.id + '"><span><b>' + t.t + '</b><span>' + t.s + '</span></span>' + I.chev + '</button></li>').join('');
let topic = null;
function show(id) {
topic = T.find(t => t.id === id);
ch.textContent = topic.t;
ways.innerHTML = topic.w.map((k, i) => {
const x = W[k];
const act = x.a ? '<a class="cpo__go" href="' + x.a + '"' + (k === 'wa' ? ' target="_blank" rel="noopener"' : '') + '>' + x.l + '</a>'
: '<button type="button" class="cpo__go" data-open="' + k + '" aria-expanded="false" aria-controls="cpo-x">' + x.l + '</button>';
return '<li class="cpo__way' + (i === 0 ? ' is-first' : '') + '"><span class="cpo__ico">' + I[x.k] + '</span><span class="cpo__txt"><b>' + x.t + (i === 0 && topic.w.length > 1 ? ' (snelst)' : '') + '</b><span>' + x.r + '</span></span>' + act + '</li>';
}).join('') + (topic.note ? '<li class="cpo__way"><span class="cpo__txt"><span>' + topic.note + '</span></span></li>' : '');
extra.innerHTML = ''; extra.id = 'cpo-x';
sTop.hidden = true; sCh.hidden = false; w.scrollTop = 0; ch.focus();
}
function back() {
sCh.hidden = true; sTop.hidden = false; extra.innerHTML = '';
const b = list.querySelector('[data-id="' + (topic && topic.id) + '"]'); if (b) b.focus();
}
list.addEventListener('click', e => { const b = e.target.closest('.cpo__topic'); if (b) show(b.dataset.id); });
w.querySelector('.cpo__back').addEventListener('click', back);
w.addEventListener('keydown', e => { if (e.key === 'Escape' && !sCh.hidden) { e.preventDefault(); back(); } });
ways.addEventListener('click', e => {
const b = e.target.closest('[data-open]'); if (!b) return;
const was = b.getAttribute('aria-expanded') === 'true';
ways.querySelectorAll('[data-open]').forEach(x => x.setAttribute('aria-expanded', 'false'));
if (was) { extra.innerHTML = ''; return; }
b.setAttribute('aria-expanded', 'true');
if (b.dataset.open === 'form') {
extra.innerHTML = '<form class="cpo__form" novalidate><label class="cpo__lbl" for="cpo-m">Je vraag</label><textarea class="cpo__in" id="cpo-m" rows="2" aria-describedby="cpo-e1"></textarea><p class="cpo__err" id="cpo-e1"></p>' +
'<label class="cpo__lbl" for="cpo-a">E-mailadres voor ons antwoord</label><input class="cpo__in" id="cpo-a" type="email" autocomplete="email" placeholder="naam@voorbeeld.nl" aria-describedby="cpo-e2"><p class="cpo__err" id="cpo-e2"></p>' +
'<button class="cpo__send" type="submit">Versturen</button></form>';
extra.querySelector('textarea').focus();
} else {
const opts = ['di 13 okt, ochtend', 'wo 14 okt, middag', 'do 15 okt, ochtend', 'vr 16 okt, middag'];
extra.innerHTML = '<p class="cpo__lbl" id="cpo-sl">Kies een dagdeel</p><div class="cpo__slots" role="radiogroup" aria-labelledby="cpo-sl">' +
opts.map((o, i) => '<button type="button" class="cpo__slot" role="radio" aria-checked="false" tabindex="' + (i ? -1 : 0) + '">' + o + '</button>').join('') +
'</div><p class="cpo__err" id="cpo-e3"></p><button class="cpo__send" type="button" data-book>Afspraak vastleggen</button>';
extra.querySelector('.cpo__slot').focus();
}
});
extra.addEventListener('click', e => {
const s = e.target.closest('.cpo__slot');
if (s) { extra.querySelectorAll('.cpo__slot').forEach(x => { x.setAttribute('aria-checked', String(x === s)); x.tabIndex = x === s ? 0 : -1; }); extra.querySelector('#cpo-e3').textContent = ''; return; }
const bk = e.target.closest('[data-book]');
if (bk) {
const c = extra.querySelector('.cpo__slot[aria-checked="true"]');
if (!c) { extra.querySelector('#cpo-e3').textContent = 'Kies eerst een dagdeel.'; extra.querySelector('.cpo__slot').focus(); return; }
done('Afspraak vastgelegd voor ' + c.textContent + '. De monteur belt een half uur van tevoren.');
}
});
extra.addEventListener('keydown', e => {
const s = e.target.closest('.cpo__slot'); if (!s || !/Arrow(Left|Right|Up|Down)/.test(e.key)) return;
e.preventDefault();
const all = [...extra.querySelectorAll('.cpo__slot')], i = all.indexOf(s);
const n = all[(i + (/Left|Up/.test(e.key) ? -1 : 1) + all.length) % all.length];
n.click(); n.focus();
});
extra.addEventListener('submit', e => {
e.preventDefault();
const m = extra.querySelector('textarea'), a = extra.querySelector('input'), btn = extra.querySelector('.cpo__send');
const e1 = m.value.trim().length < 10 ? 'Schrijf je vraag in minimaal 10 tekens.' : '';
const e2 = /^[^\s@]+@[^\s@]+\.[a-z]{2,}$/i.test(a.value.trim()) ? '' : 'Vul een geldig e-mailadres in, bijvoorbeeld naam@voorbeeld.nl.';
extra.querySelector('#cpo-e1').textContent = e1; m.setAttribute('aria-invalid', String(!!e1));
extra.querySelector('#cpo-e2').textContent = e2; a.setAttribute('aria-invalid', String(!!e2));
if (e1 || e2) { (e1 ? m : a).focus(); return; }
btn.setAttribute('aria-busy', 'true'); btn.textContent = 'Bezig met versturen...';
setTimeout(() => done('Verstuurd. Je krijgt binnen 3 werkdagen antwoord op ' + esc(a.value.trim()) + '.'), 700);
});
function done(msg) {
extra.innerHTML = '<p class="cpo__ok" tabindex="-1">' + I.ok + '<span>' + msg + '</span></p>';
ways.querySelectorAll('[data-open]').forEach(x => x.setAttribute('aria-expanded', 'false'));
extra.querySelector('.cpo__ok').focus(); sr.textContent = msg.replace(/<[^>]+>/g, '');
}
}
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 →