Lijst met detailpaneel New
Property listing where each row opens a detail panel on the right, with previous and next inside the panel (also with the left and right arrow keys), Esc or the scrim to close and focus back on the row. The panel is a
Te koop in Haarlem
8 woningenHow to use it
- Paste the HTML where the components 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.
<!-- 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'));
More components
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 →