Stap 10 — Kaartenslider met CSS Scroll Snap (geen JS)

Trendproof alternatief voor carrousels: een horizontale scroller met kaarten, gebouwd met CSS Scroll Snap. Werkt met swipe, muiswiel en toetsenbord. Geen JavaScript nodig.

10.1 Markup

Plaats deze sectie waar je de slider wil. De “bolletjes” zijn gewoon links naar elke kaart.

<section class="cardslider" aria-label="Aanbevolen projecten">

  <!-- Track: focusbaar voor toetsenbordscroll -->
  <div class="track" id="cards-track" tabindex="0">

    <article class="card" id="card-1">
      <img src="assets/img/proj1-640.jpg" width="640" height="360" alt="Poster 4GT — kleurrijke typografie">
      <div class="card-body">
        <h3 class="h6 mb-1">Poster 4GT</h3>
        <p class="mb-2 small">Kleurstudie en typografie-experiment.</p>
        <a class="btn btn-sm btn-primary" href="#">Bekijk</a>
      </div>
    </article>

    <article class="card" id="card-2">
      <img src="assets/img/proj2-640.jpg" width="640" height="360" loading="lazy" alt="Packaging — doosje met patroon">
      <div class="card-body">
        <h3 class="h6 mb-1">Packaging</h3>
        <p class="mb-2 small">Kartonnen doosje met eigen patroon.</p>
        <a class="btn btn-sm btn-primary" href="#">Bekijk</a>
      </div>
    </article>

    <article class="card" id="card-3">
      <img src="assets/img/proj3-640.jpg" width="640" height="360" loading="lazy" alt="Web lay-out — landing met hero en kaarten">
      <div class="card-body">
        <h3 class="h6 mb-1">Web lay-out</h3>
        <p class="mb-2 small">Hero + kaarten, mobile-first.</p>
        <a class="btn btn-sm btn-primary" href="#">Bekijk</a>
      </div>
    </article>

  </div>

  <!-- Dots: ankernavigatie zonder JS -->
  <nav class="dots" aria-label="Slides">
    <a href="#card-1" aria-label="Ga naar kaart 1"></a>
    <a href="#card-2" aria-label="Ga naar kaart 2"></a>
    <a href="#card-3" aria-label="Ga naar kaart 3"></a>
  </nav>

</section>

10.2 CSS in assets/css/custom.css

Scroll-snap, kaartstijl, subtiele rand-masker en toegankelijke focus.

/* ==== Cardslider (CSS-only) ==== */
.cardslider{
  --gap: 1rem;
  --card-w: clamp(260px, 45vw, 360px);
  --edge-fade: 24px; /* hint dat er meer is */
  position: relative;
}

/* Horizontale track met scroll snap */
.cardslider .track{
  display: grid;
  grid-auto-flow: column;
  grid-auto-columns: var(--card-w);
  gap: var(--gap);
  overflow-x: auto;
  padding: .5rem;
  scroll-snap-type: x mandatory;
  scroll-behavior: smooth;              /* prettige pijtjes/anker-scroll */
  -webkit-overflow-scrolling: touch;
  /* rand-fade met mask (verbergt harde randen) */
  mask-image: linear-gradient(to right,
    transparent 0, black var(--edge-fade),
    black calc(100% - var(--edge-fade)), transparent 100%);
}

/* Kaarten */
.cardslider .card{
  scroll-snap-align: start;
  border: 1px solid rgba(0,0,0,.08);
  border-radius: .75rem;
  background: var(--brand-surface);
  box-shadow: 0 8px 24px rgba(0,0,0,.06);
  overflow: hidden;
  display: flex; flex-direction: column;
}

/* Afbeelding netjes schalen */
.cardslider .card img{
  display: block; width: 100%;
  aspect-ratio: 16 / 9; object-fit: cover;
}

/* Body */
.cardslider .card .card-body{ padding: .75rem; }

/* Dots (ankers) */
.cardslider .dots{
  display: flex; justify-content: center; gap: .5rem;
  margin-top: .5rem;
}
.cardslider .dots a{
  inline-size: .6rem; block-size: .6rem;
  border-radius: 50%;
  background: color-mix(in oklab, var(--brand-text), white 70%);
  display: inline-block;
  text-decoration: none;
  outline: none;
}
.cardslider .dots a:hover,
.cardslider .dots a:focus{ 
  background: var(--brand-primary); 
  box-shadow: 0 0 0 .2rem color-mix(in oklab, var(--brand-primary), white 70%);
}

/* Active dot met :has() op het target (moderne browsers) */
@supports (selector(:has(*))){
  .cardslider:has(#card-1:target) .dots a[href="#card-1"],
  .cardslider:has(#card-2:target) .dots a[href="#card-2"],
  .cardslider:has(#card-3:target) .dots a[href="#card-3"]{
    background: var(--brand-primary);
  }
  /* Als niets getarget is, maak eerste dot actief */
  .cardslider:not(:has(.card:target)) .dots a[href="#card-1"]{
    background: var(--brand-primary);
  }
}

/* Toetsenbord: focus zichtbaar op track en kaarten */
.cardslider .track:focus{ outline: 2px solid var(--brand-primary); outline-offset: 2px; }
.cardslider .card:focus-within{ box-shadow: 0 0 0 .2rem color-mix(in oklab, var(--brand-primary), white 70%); }

/* Minder beweging = minder smooth scroll */
@media (prefers-reduced-motion: reduce){
  .cardslider .track{ scroll-behavior: auto; }
}

10.3 Hoe bedienen?

  • Touch: swipe horizontaal; de kaarten “snappen” vanzelf.
  • Muis: horizontaal scrollen (Shift + muiswiel) of sleep met een trackpad.
  • Toetsenbord: focus op de track (Tab) → gebruik pijltoetsen of PageUp/PageDown om te scrollen; of klik op de bolletjes (ankers).

10.4 Snel testen

  • Snappen de kaarten op hun begin (geen half afgesneden titels)?
  • Blijven de afbeeldingen in 16:9 en croppen ze netjes?
  • Active dot volgt het anker (kaart-ID) in moderne browsers?

Checklist

  • Geen JS; alleen CSS scroll-snap-type, scroll-behavior en (optioneel) :has().
  • Alt-teksten staan op elke kaartafbeelding.
  • prefers-reduced-motion respecteren (smooth scroll uit bij reduce).