Zoeken...  ⌘K GitHub

ImageCarousel image

ImageCarousel component.

/image-carousel
src/components/image/ImageCarousel.astro
---
/**
 * ImageCarousel — heading + cross-fade carousel met pijlen en dots.
 *
 * Props:
 * - eyebrow?: string
 * - headline?: string
 * - slides?: { src: string; alt?: string; caption?: string }[]
 * - ratio?: string                 (aspect-ratio, bv. '16/9')
 * - autoplay?: boolean
 * - interval?: number              (ms)
 */
interface Slide {
  src: string;
  alt?: string;
  caption?: string;
}
interface Props {
  eyebrow?: string;
  headline?: string;
  slides?: Slide[];
  ratio?: string;
  autoplay?: boolean;
  interval?: number;
}
const {
  eyebrow,
  headline,
  slides = [
    { src: '/img/ext/photo-1460925895917-afdab827c52f-w1200-94b636.jpg', alt: '', caption: 'Performance analyse' },
    { src: '/img/ext/photo-1542744173-8e7e53415bb0-w1200-939b04.jpg', alt: '', caption: 'Teamwork centraal' },
    { src: '/img/ext/photo-1551434678-e076c223a692-w1200-8bdd3a.jpg', alt: '', caption: 'Data-gedreven aanpak' },
  ],
  ratio = '16/9',
  autoplay = true,
  interval = 4000,
} = Astro.props;
---

<section class="bl-section icr">
  <div class="bl-inner icr__inner">
    {(eyebrow || headline) && (
      <div class="icr__header">
        {eyebrow && <p class="icr__eyebrow">{eyebrow}</p>}
        {headline && <h2 class="icr__headline">{headline}</h2>}
      </div>
    )}
    <div class="icr__wrap" data-autoplay={String(autoplay)} data-interval={String(interval)} style={`--icr-ratio: ${ratio};`}>
      <div class="icr__track">
        {slides.map((s, i) => (
          <div class:list={['icr__slide', { 'icr__slide--active': i === 0 }]} aria-hidden={i === 0 ? 'false' : 'true'}>
            <img data-allow-img src={s.src} alt={s.alt || ''} class="icr__img" loading={i === 0 ? 'eager' : 'lazy'} decoding="async" />
            {s.caption && (
              <div class="icr__caption">
                <span class="icr__caption-text">{s.caption}</span>
              </div>
            )}
          </div>
        ))}
      </div>
      <button class="icr__arrow icr__arrow--prev" aria-label="Vorige slide">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="15 18 9 12 15 6"></polyline></svg>
      </button>
      <button class="icr__arrow icr__arrow--next" aria-label="Volgende slide">
        <svg width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"><polyline points="9 18 15 12 9 6"></polyline></svg>
      </button>
      <div class="icr__dots" role="tablist" aria-label="Slide navigatie">
        {slides.map((_, i) => (
          <button class:list={['icr__dot', { 'icr__dot--active': i === 0 }]} role="tab" aria-label={`Slide ${i + 1}`} aria-selected={i === 0 ? 'true' : 'false'} data-index={String(i)}></button>
        ))}
      </div>
    </div>
  </div>
</section>

<style>
.icr{background:var(--color-bg)}
.icr__header{margin-bottom:2rem}
.icr__eyebrow{font-size:.75rem;font-weight:600;letter-spacing:.1em;text-transform:uppercase;color:var(--color-accent);margin:0 0 .5rem}
.icr__headline{font-size:clamp(1.75rem,4vw,2.75rem);font-weight:700;color:var(--color-primary);margin:0;line-height:1.15}
.icr__wrap{position:relative;border-radius:var(--radius);overflow:hidden;user-select:none}
.icr__track{position:relative;aspect-ratio:var(--icr-ratio, 16 / 9);width:100%;overflow:hidden}
.icr__slide{position:absolute;inset:0;opacity:0;transition:opacity .5s ease;pointer-events:none}
.icr__slide--active{opacity:1;pointer-events:auto}
.icr__img{width:100%;height:100%;object-fit:cover;display:block}
.icr__caption{position:absolute;bottom:0;left:0;right:0;padding:2rem 1.5rem 1rem;background:linear-gradient(to top,rgba(0,0,0,.6) 0%,transparent 100%)}
.icr__caption-text{color:#fff;font-size:.9375rem;line-height:1.5}
.icr__arrow{position:absolute;top:50%;transform:translateY(-50%);width:44px;height:44px;border-radius:50%;background:#ffffffe6;border:none;cursor:pointer;display:flex;align-items:center;justify-content:center;color:var(--color-primary);box-shadow:0 2px 8px #00000026;transition:background .2s ease,transform .2s ease;z-index:10}
.icr__arrow:hover{background:#fff;transform:translateY(-50%) scale(1.05)}
.icr__arrow--prev{left:1rem}
.icr__arrow--next{right:1rem}
.icr__dots{position:absolute;bottom:1rem;left:50%;transform:translate(-50%);display:flex;gap:.5rem;z-index:10}
.icr__dot{width:8px;height:8px;border-radius:50%;border:none;background:#ffffff80;cursor:pointer;padding:0;transition:background .2s ease,transform .2s ease}
.icr__dot--active{background:#fff;transform:scale(1.25)}
.icr__dot:hover:not(.icr__dot--active){background:#fffc}
</style>

<script>
  document.querySelectorAll('.icr__wrap').forEach((wrap) => {
    const slides = Array.from(wrap.querySelectorAll('.icr__slide'));
    const dots = Array.from(wrap.querySelectorAll('.icr__dot'));
    if (slides.length < 2) return;
    let idx = 0;
    let timer;
    const go = (n) => {
      idx = (n + slides.length) % slides.length;
      slides.forEach((s, i) => {
        s.classList.toggle('icr__slide--active', i === idx);
        s.setAttribute('aria-hidden', i === idx ? 'false' : 'true');
      });
      dots.forEach((d, i) => {
        d.classList.toggle('icr__dot--active', i === idx);
        d.setAttribute('aria-selected', i === idx ? 'true' : 'false');
      });
    };
    wrap.querySelector('.icr__arrow--prev')?.addEventListener('click', () => { go(idx - 1); restart(); });
    wrap.querySelector('.icr__arrow--next')?.addEventListener('click', () => { go(idx + 1); restart(); });
    dots.forEach((d, i) => d.addEventListener('click', () => { go(i); restart(); }));
    const autoplay = wrap.getAttribute('data-autoplay') === 'true';
    const interval = Number(wrap.getAttribute('data-interval')) || 4000;
    const start = () => { if (autoplay) timer = window.setInterval(() => go(idx + 1), interval); };
    const restart = () => { window.clearInterval(timer); start(); };
    start();
  });
</script>