Lijst met detailpaneel Nieuw
Woningaanbod als lijst waarvan elke rij een detailpaneel rechts opent, met vorige en volgende in het paneel (ook met de pijltjes links en rechts), Esc of de scrim om te sluiten en focus terug op de rij. Het paneel is een
Te koop in Haarlem
8 woningenZo 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.
<!-- Lijst met detailpaneel · NedDev · eigen werk. -->
<div class="jouw-container">
<div class="zpd">
<div class="zpd__main">
<div class="zpd__head"><h3 class="zpd__h">Te koop in Haarlem</h3><span class="zpd__n">8 woningen</span></div>
<ul class="zpd__list" role="list"></ul>
</div>
<div class="zpd__scrim" hidden></div>
<dialog class="zpd__panel" aria-labelledby="zpd-t">
<div class="zpd__bar">
<button type="button" class="zpd__ic" data-step="-1" aria-label="Vorige woning"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m14 6-6 6 6 6"/></svg></button>
<button type="button" class="zpd__ic" data-step="1" aria-label="Volgende woning"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="m10 6 6 6-6 6"/></svg></button>
<span class="zpd__pos" aria-live="polite"></span>
<button type="button" class="zpd__ic zpd__x" aria-label="Paneel sluiten"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button>
</div>
<div class="zpd__body">
<h4 class="zpd__t" id="zpd-t" tabindex="-1"></h4>
<p class="zpd__sub"></p>
<p class="zpd__price"></p>
<dl class="zpd__dl"></dl>
<p class="zpd__desc"></p>
<button type="button" class="zpd__cta">Bezichtiging aanvragen</button>
<p class="zpd__ok" aria-live="polite"></p>
</div>
</dialog>
</div>
</div>/* Lijst met detailpaneel · NedDev · eigen werk. */
.zpd{--ink:#0a0a0a;--pop:#F5480F;--pd:#b83a0b;--mut:#5b5b5b;--line:rgba(10,10,10,.12);position:absolute;inset:0;overflow:hidden;font-family:'Switzer',system-ui,sans-serif;font-size:12.5px;line-height:1.4;color:var(--ink);background:#f4f3f1;text-align:left}
.zpd *,.zpd *::before,.zpd *::after{box-sizing:border-box}
.zpd svg{width:15px;height:15px;fill:none;stroke:currentColor;stroke-width:1.8;stroke-linecap:round;stroke-linejoin:round}
.zpd__main{height:100%;display:flex;flex-direction:column;padding:10px 12px 0}
.zpd__head{display:flex;align-items:baseline;justify-content:space-between;gap:10px;padding-bottom:6px}
.zpd__h{margin:0;font-size:14px;font-weight:600}
.zpd__n{font-size:11.5px;color:var(--mut)}
.zpd__list{flex:1;min-height:0;overflow:auto;list-style:none;margin:0;padding:0 0 10px;scrollbar-width:thin}
.zpd__row{display:flex;align-items:center;gap:10px;width:100%;padding:7px 8px;margin-bottom:3px;border:1px solid transparent;border-radius:4px;background:#fff;color:var(--ink);font:inherit;text-align:left;cursor:pointer;transition:border-color .15s}
.zpd__row:hover{border-color:var(--ink)}
.zpd__row[aria-current="true"]{border-color:var(--ink);background:#fff}
.zpd__ad{flex:1;min-width:0}
.zpd__ad b{display:block;font-weight:600;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.zpd__ad span{display:block;font-size:11.5px;color:var(--mut)}
.zpd__pr{font-weight:600;font-variant-numeric:tabular-nums;white-space:nowrap;text-align:right}
.zpd__pr span{display:block;font-weight:500;font-size:11px;color:var(--mut)}
.zpd__pr span.is-bod{color:var(--pd)}
.zpd button:focus-visible{outline:2px solid var(--pop);outline-offset:2px}
.zpd__scrim{position:absolute;inset:0;background:rgba(10,10,10,.35);opacity:1;transition:opacity .2s}
.zpd__scrim[hidden]{display:none}
@starting-style{.zpd__scrim{opacity:0}}
.zpd__panel{position:absolute;inset:0 0 0 auto;margin:0;padding:0;width:min(290px,90%);height:100%;max-height:none;border:0;border-left:1px solid var(--ink);border-radius:0;background:#fff;color:var(--ink);display:none;flex-direction:column;translate:0 0;transition:translate .22s cubic-bezier(.2,.7,.2,1)}
.zpd__panel[open]{display:flex}
@starting-style{.zpd__panel[open]{translate:100% 0}}
.zpd__bar{display:flex;align-items:center;gap:4px;padding:8px 10px;border-bottom:1px solid var(--line)}
.zpd__ic{display:grid;place-items:center;width:28px;height:28px;border:1px solid var(--line);border-radius:4px;background:#fff;color:var(--ink);cursor:pointer;transition:border-color .15s}
.zpd__ic:hover:not(:disabled){border-color:var(--ink)}
.zpd__ic:disabled{opacity:.35;cursor:not-allowed}
.zpd__pos{margin-left:4px;font-size:11.5px;color:var(--mut);font-variant-numeric:tabular-nums}
.zpd__x{margin-left:auto;border-color:transparent}
.zpd__body{flex:1;min-height:0;overflow:auto;padding:10px 12px 12px;scrollbar-width:thin}
.zpd__t{margin:0;font-size:15px;font-weight:600;letter-spacing:-.01em}
.zpd__t:focus{outline:none}
.zpd__sub{margin:0;color:var(--mut)}
.zpd__price{margin:6px 0 8px;font-size:16px;font-weight:700;font-variant-numeric:tabular-nums}
.zpd__dl{display:grid;grid-template-columns:auto 1fr;gap:3px 10px;margin:0 0 8px;padding:8px 0;border-top:1px solid var(--line);border-bottom:1px solid var(--line)}
.zpd__dl dt{color:var(--mut)}
.zpd__dl dd{margin:0;font-weight:500;display:flex;align-items:center;gap:6px}
.zpd__lab{display:inline-block;width:18px;height:10px;clip-path:polygon(0 0,75% 0,100% 50%,75% 100%,0 100%)}
.zpd__desc{margin:0 0 10px;color:#333}
.zpd__cta{width:100%;height:34px;border:0;border-radius:4px;background:var(--ink);color:#fff;font:600 12.5px/1 'Switzer',system-ui,sans-serif;cursor:pointer;transition:background-color .15s}
.zpd__cta:hover{background:#2a2a2a}
.zpd__ok{margin:6px 0 0;font-size:12px}
.zpd__ok:empty{display:none}
@media (prefers-reduced-motion:reduce){.zpd__panel,.zpd__scrim,.zpd *{transition:none!important}}// Lijst met detailpaneel · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
const w = root.querySelector('.zpd'); if (!w) return;
const main = w.querySelector('.zpd__main'), list = w.querySelector('.zpd__list'), panel = w.querySelector('.zpd__panel'), scrim = w.querySelector('.zpd__scrim');
const eur = n => '€ ' + n.toLocaleString('nl-NL') + ' k.k.';
const LABC = { A: '#1a7f3c', B: '#55a234', C: '#8a9a1a', D: '#b78a00', E: '#c8641a', F: '#c03a1a' };
const H = [
['Kleverparkweg 41', '2023 CD', 'Kleverpark', 549000, 'Te koop', 112, 4, 1928, 'C', 'Jaren-dertigwoning met originele glas-in-lood en een tuin op het zuiden. Zolder is als vierde slaapkamer in gebruik.'],
['Zijlweg 188 rd', '2015 BN', 'Leidsebuurt', 425000, 'Onder bod', 78, 3, 1905, 'D', 'Benedenwoning met eigen voordeur en achtertuin van 11 meter. Tien minuten lopen naar station Haarlem.'],
['Spaarndamseweg 7', '2021 KA', 'Transvaalwijk', 389000, 'Te koop', 69, 3, 1930, 'C', 'Bovenwoning met dakterras en nieuwe keuken uit 2024. VvE is actief en gezond.'],
['Rijksstraatweg 302', '2025 DK', 'Haarlem-Noord', 675000, 'Te koop', 134, 5, 1964, 'B', 'Ruime hoekwoning met garage en vrij uitzicht aan de achterzijde. Zonnepanelen (12 stuks).'],
['Botermarkt 12', '2011 XL', 'Centrum', 495000, 'Verkocht o.v.', 74, 2, 1650, 'E', 'Monumentaal appartement boven een winkel aan de Botermarkt, met balkenplafond en schouw.'],
['Schoterweg 64', '2021 HJ', 'Indischewijk', 459000, 'Te koop', 96, 4, 1925, 'C', 'Tussenwoning met uitbouw en een bijkeuken. Basisschool en supermarkt om de hoek.'],
['Floraplein 3', '2012 HT', 'Rozenprieel', 579000, 'Te koop', 105, 4, 2008, 'A', 'Nieuwbouw uit 2008 met vloerverwarming, warmtepomp en parkeerplaats in de binnentuin.'],
['Generaal Cronjestraat 90', '2021 JN', 'Transvaalwijk', 349000, 'Onder bod', 58, 2, 1912, 'D', 'Compact appartement in de winkelstraat, met berging en ruim balkon op het westen.']
];
list.innerHTML = H.map((h, i) => '<li><button type="button" class="zpd__row" data-i="' + i + '" aria-haspopup="dialog"><span class="zpd__ad"><b>' + h[0] + '</b><span>' + h[1] + ' Haarlem · ' + h[2] + '</span></span><span class="zpd__pr">' + eur(h[3]).replace(' k.k.', '') + '<span' + (h[4] !== 'Te koop' ? ' class="is-bod"' : '') + '>' + h[4] + '</span></span></button></li>').join('');
let cur = -1, opener = null;
function fill(i) {
cur = i; const h = H[i];
panel.querySelector('.zpd__t').textContent = h[0];
panel.querySelector('.zpd__sub').textContent = h[1] + ' Haarlem, ' + h[2];
panel.querySelector('.zpd__price').textContent = eur(h[3]);
panel.querySelector('.zpd__dl').innerHTML = '<dt>Status</dt><dd>' + h[4] + '</dd><dt>Woonoppervlak</dt><dd>' + h[5] + ' m²</dd><dt>Kamers</dt><dd>' + h[6] + '</dd><dt>Bouwjaar</dt><dd>' + h[7] + '</dd><dt>Energielabel</dt><dd><span class="zpd__lab" style="background:' + LABC[h[8]] + '" aria-hidden="true"></span>' + h[8] + '</dd>';
panel.querySelector('.zpd__desc').textContent = h[9];
const cta = panel.querySelector('.zpd__cta'); cta.disabled = h[4] === 'Verkocht o.v.'; cta.textContent = cta.disabled ? 'Verkocht onder voorbehoud' : 'Bezichtiging aanvragen';
panel.querySelector('.zpd__ok').textContent = '';
panel.querySelector('.zpd__pos').textContent = (i + 1) + ' van ' + H.length;
panel.querySelector('[data-step="-1"]').disabled = i === 0;
panel.querySelector('[data-step="1"]').disabled = i === H.length - 1;
list.querySelectorAll('.zpd__row').forEach(r => r.setAttribute('aria-current', String(+r.dataset.i === i)));
panel.querySelector('.zpd__body').scrollTop = 0;
}
function open(i, from) {
opener = from; fill(i);
main.inert = true; scrim.hidden = false;
if (panel.show) panel.show(); else panel.setAttribute('open', '');
panel.querySelector('.zpd__t').focus();
}
function close() {
if (!panel.hasAttribute('open')) return;
if (panel.close) panel.close(); else panel.removeAttribute('open');
main.inert = false; scrim.hidden = true;
const r = list.querySelector('[data-i="' + cur + '"]'); if (r) { r.focus(); r.scrollIntoView({ block: 'nearest' }); }
}
function step(d) {
const n = cur + d; if (n < 0 || n >= H.length) return;
fill(n);
const t = panel.querySelector('.zpd__t');
if (panel.contains(document.activeElement) && document.activeElement.disabled) t.focus();
}
list.addEventListener('click', e => { const r = e.target.closest('.zpd__row'); if (r) open(+r.dataset.i, r); });
panel.addEventListener('click', e => {
const s = e.target.closest('[data-step]'); if (s) { step(+s.dataset.step); if (s.disabled) panel.querySelector('.zpd__t').focus(); return; }
if (e.target.closest('.zpd__x')) { close(); return; }
if (e.target.closest('.zpd__cta')) panel.querySelector('.zpd__ok').textContent = 'Aanvraag verstuurd. De makelaar belt je binnen 1 werkdag voor een moment.';
});
scrim.addEventListener('click', close);
// Beginstand in de demo: paneel open op de eerste woning, zonder focus te stelen.
fill(0); main.inert = true; scrim.hidden = false; panel.setAttribute('open', '');
panel.addEventListener('cancel', e => { e.preventDefault(); close(); });
panel.addEventListener('keydown', e => {
if (e.key === 'Escape') { e.preventDefault(); close(); return; }
if ((e.key === 'ArrowLeft' || e.key === 'ArrowRight') && !e.altKey) { e.preventDefault(); step(e.key === 'ArrowLeft' ? -1 : 1); return; }
if (e.key === 'Tab') {
const f = [...panel.querySelectorAll('button:not(:disabled),[tabindex="-1"]')].filter(x => x.offsetParent !== null && x.tagName === 'BUTTON');
if (!f.length) return;
const i = f.indexOf(document.activeElement);
if (e.shiftKey && i <= 0) { e.preventDefault(); f[f.length - 1].focus(); }
else if (!e.shiftKey && i === f.length - 1) { e.preventDefault(); f[0].focus(); }
}
});
}
init(document.querySelector('.jouw-container'));
Meer uit componenten
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 →