FeaturesShowcase Sections
Interactieve feature showcase: scrollbare lijst links, sticky preview panel rechts. Wisselt automatisch of via klik.
src/components/sections/FeaturesShowcase.astro
---
/**
* FeaturesShowcase
* Interactieve feature showcase: links scrollbare lijst met tab-achtige selectie,
* rechts grote animated preview/afbeelding die wisselt. Sticky scroll effect.
* Puur CSS — geen JS vereist voor de basis layout.
*/
interface Feature {
number?: string;
title: string;
description: string;
imageSrc?: string;
imageAlt?: string;
tag?: string;
}
interface Props {
preHeadline?: string;
headline: string;
sub?: string;
features: Feature[];
}
const {
preHeadline,
headline,
sub,
features = [],
} = Astro.props;
---
<section class="fs" data-component="features-showcase">
<div class="fs__inner">
<!-- Header -->
<div class="fs__header">
{preHeadline && <p class="fs__pre">{preHeadline}</p>}
<h2 class="fs__headline" set:html={headline} />
{sub && <p class="fs__sub">{sub}</p>}
</div>
<!-- Showcase grid -->
<div class="fs__body">
<!-- Feature list (left) -->
<div class="fs__list">
{features.map((f, i) => (
<div class="fs__item" data-index={i}>
<div class="fs__item-header">
{f.number && <span class="fs__num">{f.number}</span>}
{f.tag && <span class="fs__tag">{f.tag}</span>}
</div>
<h3 class="fs__title">{f.title}</h3>
<p class="fs__desc">{f.description}</p>
<div class="fs__progress"><div class="fs__progress-bar"></div></div>
</div>
))}
</div>
<!-- Preview panel (right, sticky) -->
<div class="fs__preview-wrap">
<div class="fs__preview-stack">
{features.map((f, i) => (
<div class="fs__preview-slide" data-slide={i} class:list={[{ 'fs__preview-slide--active': i === 0 }]}>
{f.imageSrc ? (
<img src={f.imageSrc} alt={f.imageAlt ?? f.title} class="fs__preview-img" loading="lazy" />
) : (
<div class="fs__preview-placeholder">
<span class="fs__preview-num">{f.number ?? String(i + 1).padStart(2, '0')}</span>
<span class="fs__preview-title-lg">{f.title}</span>
</div>
)}
</div>
))}
</div>
</div>
</div>
</div>
</section>
<script>
const section = document.querySelector('[data-component="features-showcase"]');
if (section) {
const items = section.querySelectorAll<HTMLElement>('.fs__item');
const slides = section.querySelectorAll<HTMLElement>('.fs__preview-slide');
function activate(index: number) {
items.forEach((item, i) => {
item.classList.toggle('fs__item--active', i === index);
});
slides.forEach((slide, i) => {
slide.classList.toggle('fs__preview-slide--active', i === index);
});
}
// Click to switch
items.forEach((item, i) => {
item.addEventListener('click', () => activate(i));
});
// IntersectionObserver for scroll-based activation
const observer = new IntersectionObserver(
(entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const index = Number((entry.target as HTMLElement).dataset.index);
activate(index);
}
});
},
{ threshold: 0.6 }
);
items.forEach(item => observer.observe(item));
activate(0);
}
</script>
<style>
.fs {
padding: 6rem 1.5rem;
background: var(--color-bg, #fff);
overflow: hidden;
}
.fs__inner { max-width: 1200px; margin: 0 auto; }
/* Header */
.fs__header {
text-align: center;
max-width: 640px;
margin: 0 auto 5rem;
}
.fs__pre {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.12em;
text-transform: uppercase;
color: var(--color-accent, #6366f1);
margin-bottom: 1rem;
}
.fs__headline {
font-size: clamp(2rem, 4vw, 3.25rem);
font-weight: 800;
line-height: 1.1;
letter-spacing: -0.035em;
color: var(--color-primary, #0a0a0a);
margin-bottom: 1rem;
}
.fs__headline :global(em) {
font-style: normal;
color: var(--color-accent, #6366f1);
}
.fs__sub {
font-size: 1.0625rem;
line-height: 1.65;
color: var(--color-muted, #6b7280);
}
/* Body grid */
.fs__body {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 4rem;
align-items: start;
}
/* Feature list */
.fs__list {
display: flex;
flex-direction: column;
gap: 0;
}
.fs__item {
padding: 2rem 0;
border-top: 1px solid rgba(0,0,0,0.07);
cursor: pointer;
transition: padding 0.3s;
}
.fs__item:last-child { border-bottom: 1px solid rgba(0,0,0,0.07); }
.fs__item--active {
padding: 2rem 1.5rem;
background: var(--color-bg, #fff);
border-radius: 0.75rem;
box-shadow: 0 2px 24px rgba(0,0,0,0.06);
border-top-color: transparent;
}
.fs__item-header {
display: flex;
align-items: center;
gap: 0.75rem;
margin-bottom: 0.625rem;
}
.fs__num {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.1em;
color: var(--color-muted, #6b7280);
font-feature-settings: "tnum";
}
.fs__tag {
font-size: 0.6875rem;
font-weight: 700;
letter-spacing: 0.06em;
text-transform: uppercase;
background: rgba(99,102,241,0.08);
color: var(--color-accent, #6366f1);
padding: 0.1875rem 0.5rem;
border-radius: 999px;
}
.fs__title {
font-size: 1.25rem;
font-weight: 700;
letter-spacing: -0.02em;
color: var(--color-primary, #0a0a0a);
margin-bottom: 0.5rem;
}
.fs__desc {
font-size: 0.9375rem;
line-height: 1.65;
color: var(--color-muted, #6b7280);
max-height: 0;
overflow: hidden;
transition: max-height 0.35s cubic-bezier(0.22,1,0.36,1), opacity 0.3s;
opacity: 0;
}
.fs__item--active .fs__desc {
max-height: 160px;
opacity: 1;
}
/* Progress bar */
.fs__progress {
height: 2px;
background: rgba(0,0,0,0.06);
border-radius: 1px;
margin-top: 1rem;
overflow: hidden;
opacity: 0;
transition: opacity 0.3s;
}
.fs__item--active .fs__progress { opacity: 1; }
.fs__progress-bar {
height: 100%;
background: var(--color-accent, #6366f1);
border-radius: 1px;
width: 0%;
}
.fs__item--active .fs__progress-bar {
animation: fs-progress 5s linear forwards;
}
@keyframes fs-progress {
from { width: 0%; }
to { width: 100%; }
}
/* Preview panel */
.fs__preview-wrap {
position: sticky;
top: 2rem;
}
.fs__preview-stack {
position: relative;
width: 100%;
aspect-ratio: 4/3;
border-radius: 1rem;
overflow: hidden;
background: #f3f4f6;
box-shadow: 0 8px 48px rgba(0,0,0,0.1);
}
.fs__preview-slide {
position: absolute;
inset: 0;
opacity: 0;
transform: scale(0.97) translateY(8px);
transition: opacity 0.5s cubic-bezier(0.22,1,0.36,1), transform 0.5s cubic-bezier(0.22,1,0.36,1);
pointer-events: none;
}
.fs__preview-slide--active {
opacity: 1;
transform: scale(1) translateY(0);
pointer-events: auto;
}
.fs__preview-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.fs__preview-placeholder {
width: 100%;
height: 100%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 1rem;
background: linear-gradient(135deg, #f0f1ff, #e8eaff);
padding: 2rem;
text-align: center;
}
.fs__preview-num {
font-size: 4rem;
font-weight: 900;
color: var(--color-accent, #6366f1);
opacity: 0.2;
line-height: 1;
letter-spacing: -0.05em;
}
.fs__preview-title-lg {
font-size: 1.5rem;
font-weight: 700;
color: var(--color-primary, #0a0a0a);
letter-spacing: -0.02em;
}
@media (max-width: 900px) {
.fs__body { grid-template-columns: 1fr; }
.fs__preview-wrap { position: relative; top: 0; order: -1; }
.fs__preview-stack { aspect-ratio: 16/9; }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
features * | Feature[] | — | Feature items met title, description en optioneel imageSrc |
preHeadline | string | — | Label boven headline |
headline | string | — | Sectie headline |
sub | string | — | Ondertitel |
* = verplicht