Raster filteren en sorteren New
Project grid of 12 cards filtered by category and sorted by date or price; cards glide to their new spot, filtered ones fade. View Transitions with view-transition-class, fallback a small FLIP script (Web Animations), instant with reduced motion. An empty category shows an empty state with reset button. For URL-based filters on a multi-page site: @view-transition { navigation: auto; } and the same view-transition-name per card on both pages.
Nog geen dakkapellen in dit overzicht.
How 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.
<!-- Raster filteren en sorteren · NedDev · eigen werk. -->
<div class="jouw-container">
<!-- Same-document: elke kaart krijgt tijdens de overgang view-transition-name vtg-c1..vtg-c12 en view-transition-class vtg-card.
Multi-page (filter als /projecten?cat=keuken): @view-transition { navigation: auto; } en op beide pagina's per project dezelfde
view-transition-name (bv. via style="view-transition-name: project-12"). Zonder support: FLIP-script hieronder, of gewoon direct.
Reduced motion: @media (prefers-reduced-motion: reduce) { ::view-transition-group(*), ::view-transition-old(*), ::view-transition-new(*) { animation: none !important; } } -->
<div class="vtg">
<div class="vtg__bar">
<div class="vtg__chips" role="group" aria-label="Filter op categorie">
<button class="vtg__chip" type="button" data-f="alles" aria-pressed="true">Alles <span>12</span></button>
<button class="vtg__chip" type="button" data-f="bad" aria-pressed="false">Badkamer <span>4</span></button>
<button class="vtg__chip" type="button" data-f="keuken" aria-pressed="false">Keuken <span>4</span></button>
<button class="vtg__chip" type="button" data-f="tuin" aria-pressed="false">Tuin <span>4</span></button>
<button class="vtg__chip" type="button" data-f="dak" aria-pressed="false">Dakkapel <span>0</span></button>
</div>
<label class="vtg__sort"><span class="vtg__sr">Sorteer</span><select class="vtg__sel"><option value="datum">Nieuwste eerst</option><option value="laag">Prijs oplopend</option><option value="hoog">Prijs aflopend</option></select></label>
</div>
<ul class="vtg__grid" aria-label="Projecten"></ul>
<div class="vtg__leeg" hidden><p>Nog geen dakkapellen in dit overzicht.</p><button class="vtg__reset" type="button">Toon alle projecten</button></div>
<span class="vtg__sr vtg__live" aria-live="polite"></span>
</div>
</div>/* Raster filteren en sorteren · NedDev · eigen werk. */
.vtg{position:absolute;inset:0;display:flex;flex-direction:column;padding:11px 12px 12px;background:#fff;color:#0a0a0a;font-family:'Switzer',system-ui,sans-serif;overflow:hidden}
.vtg__bar{display:flex;justify-content:space-between;align-items:center;gap:6px;margin-bottom:9px;flex-wrap:wrap}
.vtg__chips{display:flex;gap:4px;flex-wrap:wrap}
.vtg__chip{all:unset;box-sizing:border-box;font-size:11.5px;font-weight:500;padding:4px 8px;border-radius:999px;border:1px solid rgba(10,10,10,.16);cursor:pointer;white-space:nowrap;transition:background-color .15s,color .15s,border-color .15s}
.vtg__chip span{font-family:'JetBrains Mono',monospace;font-size:10px;color:rgba(10,10,10,.5);margin-left:2px}
.vtg__chip:hover{border-color:#0a0a0a}
.vtg__chip[aria-pressed="true"]{background:#0a0a0a;color:#fff;border-color:#0a0a0a}
.vtg__chip[aria-pressed="true"] span{color:rgba(255,255,255,.65)}
.vtg__chip:focus-visible,.vtg__sel:focus-visible,.vtg__reset:focus-visible{outline:2px solid #F5480F;outline-offset:2px}
.vtg__sel{font:500 11.5px 'Switzer',system-ui,sans-serif;color:#0a0a0a;background:#f4f3f1;border:1px solid rgba(10,10,10,.16);border-radius:4px;padding:4px 6px;cursor:pointer}
.vtg__grid{list-style:none;margin:0;padding:0;flex:1;min-height:0;display:grid;grid-template-columns:repeat(4,1fr);grid-template-rows:repeat(3,1fr);gap:6px}
.vtg__card{position:relative;border-radius:4px;overflow:hidden;min-height:0;color:#fff;background:var(--vtg-c)}
.vtg__card::before{content:'';position:absolute;inset:0;background:radial-gradient(120% 90% at 80% 10%,rgba(255,255,255,.28),transparent 55%),linear-gradient(180deg,transparent 35%,rgba(10,10,10,.55))}
.vtg__card[data-c="bad"]{--vtg-c:linear-gradient(135deg,#9aa7ad,#5f6d74)}
.vtg__card[data-c="keuken"]{--vtg-c:linear-gradient(135deg,#c49a6c,#7d5a3a)}
.vtg__card[data-c="tuin"]{--vtg-c:linear-gradient(135deg,#7f9a62,#3f5a33)}
.vtg__tag{position:absolute;top:5px;left:6px;font:600 8.5px 'JetBrains Mono',monospace;letter-spacing:.1em;text-transform:uppercase;opacity:.9}
.vtg__cap{position:absolute;left:6px;right:6px;bottom:5px;display:flex;flex-direction:column;gap:1px;font-size:10.5px;font-weight:600;line-height:1.15;white-space:nowrap;overflow:hidden;text-overflow:ellipsis}
.vtg__cap b{font-weight:600;overflow:hidden;text-overflow:ellipsis}
.vtg__cap em{font:500 9.5px 'JetBrains Mono',monospace;font-style:normal;font-variant-numeric:tabular-nums;opacity:.9}
.vtg__leeg{flex:1;display:flex;flex-direction:column;align-items:center;justify-content:center;gap:8px;border:1px dashed rgba(10,10,10,.2);border-radius:4px;font-size:12.5px;color:rgba(10,10,10,.6);text-align:center;padding:10px}
.vtg__leeg[hidden]{display:none}.vtg__leeg p{margin:0}
.vtg__reset{all:unset;box-sizing:border-box;font-size:12px;font-weight:600;padding:6px 11px;border-radius:4px;background:#0a0a0a;color:#fff;cursor:pointer;transition:background-color .15s}
.vtg__reset:hover{background:#F5480F}
.vtg__sr{position:absolute;width:1px;height:1px;overflow:hidden;clip-path:inset(50%);white-space:nowrap}
@media (max-width:420px){.vtg{padding:9px 10px 10px}.vtg__chip{font-size:10.5px;padding:3px 7px}.vtg__chip span{display:none}.vtg__sel{font-size:10.5px;padding:3px 4px}.vtg__bar{margin-bottom:7px;row-gap:5px}.vtg__cap{font-size:9.5px}.vtg__tag{display:none}.vtg__grid{gap:5px}}
::view-transition-group(*.vtg-card){animation-duration:.42s;animation-timing-function:cubic-bezier(.2,.75,.15,1)}
::view-transition-old(*.vtg-card):only-child{animation:vtg-uit .22s ease-in both}
::view-transition-new(*.vtg-card):only-child{animation:vtg-in .34s cubic-bezier(.2,.7,.2,1) .12s both}
@keyframes vtg-uit{to{opacity:0;transform:scale(.85)}}
@keyframes vtg-in{from{opacity:0;transform:scale(.85)}}
@media (prefers-reduced-motion:reduce){::view-transition-group(*.vtg-card),::view-transition-old(*.vtg-card),::view-transition-new(*.vtg-card){animation:none!important}}// Raster filteren en sorteren · NedDev · eigen werk.
// root = het element waarin de HTML staat
function init(root){
var w=root.querySelector('.vtg');if(!w)return;var grid=w.querySelector('.vtg__grid'),leeg=w.querySelector('.vtg__leeg'),sel=w.querySelector('.vtg__sel'),live=w.querySelector('.vtg__live');
var chips=[].slice.call(w.querySelectorAll('.vtg__chip'));var rm=matchMedia('(prefers-reduced-motion: reduce)');
var TAG={bad:'Badkamer',keuken:'Keuken',tuin:'Tuin'};
var P=[['bad','Delft',8900,'2026-09'],['keuken','Utrecht',14200,'2026-08'],['tuin','Haarlem',6400,'2026-08'],['bad','Leiden',11750,'2026-07'],['keuken','Zwolle',18400,'2026-06'],['tuin','Amersfoort',9800,'2026-06'],['bad','Gouda',7300,'2026-05'],['keuken','Breda',12650,'2026-04'],['tuin','Alkmaar',4950,'2026-03'],['bad','Den Bosch',15100,'2026-02'],['keuken','Arnhem',9900,'2026-01'],['tuin','Deventer',13300,'2025-12']];
var f='alles',s='datum',busy=false;
P.forEach(function(p,i){var li=document.createElement('li');li.className='vtg__card';li.dataset.c=p[0];li.dataset.i=i;
li.innerHTML='<span class="vtg__tag">'+TAG[p[0]]+'</span><span class="vtg__cap"><b>'+p[1]+'</b><em>€ '+p[2].toLocaleString('nl-NL')+'</em></span>';
li.setAttribute('aria-label',TAG[p[0]]+' '+p[1]+', € '+p[2].toLocaleString('nl-NL'));grid.appendChild(li);});
var cards=[].slice.call(grid.children);
function apply(){var order=cards.slice().sort(function(a,b){var x=P[a.dataset.i],y=P[b.dataset.i];return s==='laag'?x[2]-y[2]:s==='hoog'?y[2]-x[2]:(x[3]<y[3]?1:x[3]>y[3]?-1:a.dataset.i-b.dataset.i);});
var n=0;order.forEach(function(c){var on=f==='alles'||c.dataset.c===f;c.hidden=!on;if(on)n++;grid.appendChild(c);});
grid.hidden=n===0;leeg.hidden=n>0;chips.forEach(function(c){c.setAttribute('aria-pressed',c.dataset.f===f?'true':'false');});live.textContent=n+(n===1?' project':' projecten');}
function names(on){cards.forEach(function(c){c.style.viewTransitionName=on?'vtg-c'+c.dataset.i:'';c.style.viewTransitionClass=on?'vtg-card':'';});}
function flip(){var first=new Map();cards.forEach(function(c){if(!c.hidden)first.set(c,c.getBoundingClientRect());});apply();
cards.forEach(function(c){if(c.hidden)return;var a=first.get(c),b=c.getBoundingClientRect();if(a){var dx=a.left-b.left,dy=a.top-b.top;if(dx||dy)c.animate([{transform:'translate('+dx+'px,'+dy+'px)'},{transform:'none'}],{duration:420,easing:'cubic-bezier(.2,.75,.15,1)'});}
else c.animate([{opacity:0,transform:'scale(.85)'},{opacity:1,transform:'none'}],{duration:340,delay:120,easing:'cubic-bezier(.2,.7,.2,1)',fill:'backwards'});});}
function change(){if(busy){apply();return;}if(rm.matches){apply();return;}if(!document.startViewTransition||!grid.animate||w.dataset.flip){flip();return;}
busy=true;names(true);var t=document.startViewTransition(apply);t.finished.finally(function(){names(false);busy=false;});}
chips.forEach(function(c){c.addEventListener('click',function(){if(f===c.dataset.f)return;f=c.dataset.f;change();});});
sel.addEventListener('change',function(){s=sel.value;change();});
w.querySelector('.vtg__reset').addEventListener('click',function(){f='alles';change();chips[0].focus({preventScroll:true});});
apply();
}
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 →