FeaturesBento Sections
Bento-grid layout met variabele cel-groottes — tekst, stats, quotes en afbeeldingen.
src/components/sections/FeaturesBento.astro
---
/**
* FeaturesBento
* Bento-grid layout — visueel rijke feature showcase met variabele cel-groottes.
*/
interface BentoItem {
size: 'sm' | 'md' | 'lg' | 'wide' | 'tall';
type: 'text' | 'image' | 'stat' | 'quote';
headline?: string;
body?: string;
image?: string;
imageAlt?: string;
stat?: string;
statLabel?: string;
quote?: string;
quoteAuthor?: string;
accent?: boolean;
dark?: boolean;
}
interface Props {
eyebrow?: string;
headline?: string;
items: BentoItem[];
}
const { eyebrow, headline, items = [] } = Astro.props;
---
<section class="fbt" data-component="features-bento">
<div class="fbt__inner">
{(eyebrow || headline) && (
<div class="fbt__header">
{eyebrow && <p class="fbt__eyebrow">{eyebrow}</p>}
{headline && <h2 class="fbt__title" set:html={headline} />}
</div>
)}
<div class="fbt__grid">
{items.map(item => (
<div class:list={[
'fbt__cell',
`fbt__cell--${item.size}`,
{ 'fbt__cell--accent': item.accent },
{ 'fbt__cell--dark': item.dark },
{ 'fbt__cell--image': item.type === 'image' },
]}>
{item.type === 'image' && item.image && (
<img src={item.image} alt={item.imageAlt ?? ''} class="fbt__cell-img" />
)}
{item.type === 'stat' && (
<div class="fbt__stat">
<div class="fbt__stat-value">{item.stat}</div>
<div class="fbt__stat-label">{item.statLabel}</div>
</div>
)}
{item.type === 'quote' && (
<div class="fbt__quote">
<svg width="24" height="24" viewBox="0 0 24 24" fill="currentColor" opacity="0.3">
<path d="M14.017 21v-7.391c0-5.704 3.731-9.57 8.983-10.609l.995 2.151c-2.432.917-3.995 3.638-3.995 5.849h4v10h-9.983zm-14.017 0v-7.391c0-5.704 3.748-9.57 9-10.609l.996 2.151c-2.433.917-3.996 3.638-3.996 5.849h3.983v10h-9.983z"/>
</svg>
<p class="fbt__quote-text">{item.quote}</p>
{item.quoteAuthor && <p class="fbt__quote-author">— {item.quoteAuthor}</p>}
</div>
)}
{item.type === 'text' && (
<div class="fbt__text">
{item.headline && <h3 class="fbt__cell-headline">{item.headline}</h3>}
{item.body && <p class="fbt__cell-body">{item.body}</p>}
</div>
)}
</div>
))}
</div>
</div>
</section>
<style>
.fbt {
background: #f5f5f7;
padding: 5rem 1.5rem;
}
.fbt__inner {
max-width: 1200px;
margin: 0 auto;
}
.fbt__header {
text-align: center;
margin-bottom: 3rem;
}
.fbt__eyebrow {
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent, #6366f1);
margin-bottom: 0.75rem;
}
.fbt__title {
font-size: clamp(1.875rem, 3vw, 2.75rem);
font-weight: 800;
letter-spacing: -0.03em;
color: var(--color-primary, #0a0a0a);
}
.fbt__title :global(em) {
font-style: normal;
color: var(--color-accent, #6366f1);
}
/* Grid */
.fbt__grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 200px;
gap: 1rem;
}
.fbt__cell {
background: #fff;
border-radius: calc(var(--radius, 0.5rem) * 2);
padding: 1.75rem;
overflow: hidden;
position: relative;
display: flex;
align-items: flex-end;
}
.fbt__cell--sm { grid-column: span 1; }
.fbt__cell--md { grid-column: span 2; }
.fbt__cell--lg { grid-column: span 2; grid-row: span 2; }
.fbt__cell--wide { grid-column: span 3; }
.fbt__cell--tall { grid-column: span 1; grid-row: span 2; }
.fbt__cell--accent {
background: var(--color-accent, #6366f1);
color: #fff;
}
.fbt__cell--dark {
background: var(--color-primary, #0a0a0a);
color: #fff;
}
.fbt__cell--image {
padding: 0;
align-items: stretch;
}
.fbt__cell-img {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: inherit;
}
/* Stat */
.fbt__stat {
display: flex;
flex-direction: column;
width: 100%;
}
.fbt__stat-value {
font-size: 3.5rem;
font-weight: 800;
letter-spacing: -0.04em;
line-height: 1;
margin-bottom: 0.375rem;
}
.fbt__cell--accent .fbt__stat-value { color: #fff; }
.fbt__cell--dark .fbt__stat-value { color: #fff; }
.fbt__stat-label {
font-size: 0.875rem;
opacity: 0.6;
}
/* Quote */
.fbt__quote {
display: flex;
flex-direction: column;
width: 100%;
gap: 0.75rem;
}
.fbt__quote-text {
font-size: 1rem;
line-height: 1.6;
font-weight: 500;
}
.fbt__quote-author {
font-size: 0.8125rem;
opacity: 0.55;
}
/* Text */
.fbt__text { width: 100%; }
.fbt__cell-headline {
font-size: 1.125rem;
font-weight: 700;
margin-bottom: 0.5rem;
color: var(--color-primary, #0a0a0a);
}
.fbt__cell--accent .fbt__cell-headline,
.fbt__cell--dark .fbt__cell-headline { color: #fff; }
.fbt__cell-body {
font-size: 0.9rem;
line-height: 1.6;
color: var(--color-muted, #6b7280);
}
.fbt__cell--accent .fbt__cell-body,
.fbt__cell--dark .fbt__cell-body { color: rgba(255,255,255,0.65); }
@media (max-width: 900px) {
.fbt__grid { grid-template-columns: repeat(2, 1fr); }
.fbt__cell--wide { grid-column: span 2; }
.fbt__cell--lg { grid-column: span 2; }
}
@media (max-width: 540px) {
.fbt__grid { grid-template-columns: 1fr; grid-auto-rows: auto; }
.fbt__cell { min-height: 160px; }
.fbt__cell--sm,
.fbt__cell--md,
.fbt__cell--lg,
.fbt__cell--wide,
.fbt__cell--tall { grid-column: span 1; grid-row: span 1; }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
items * | BentoItem[] | — | Grid items. Grootte: sm/md/lg/wide/tall. Type: text/image/stat/quote. |
eyebrow | string | — | Label boven sectie |
headline | string | — | Sectie headline |
* = verplicht