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>