Contact per onderwerp Nieuw
Contactblok dat eerst vraagt waar het over gaat en daarna alleen de kanalen toont die bij dat onderwerp passen (bellen, WhatsApp, formulier, afspraak), elk met een eerlijke reactietijd en of de telefoon nu bezet is. Het formulier en de afspraakkeuze werken inline met fout- en succesmelding. Focus springt mee bij elke stap, terug gaat naar het gekozen onderwerp. Koppel in productie de openingstijden en tijdsloten aan je planning.
Waar gaat je vraag over?
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.
<!-- 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'));
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 →