IconGrid icon
Grid van icoon + titel + beschrijving. 4 icoon stijlen (plain, circle, square, outlined). Staggered fade-in.
src/components/icon/IconGrid.astro
---
interface Props {
eyebrow?: string;
headline?: string;
sub?: string;
items: {
icon: string;
title: string;
description: string;
href?: string;
}[];
columns?: 2 | 3 | 4;
iconStyle?: 'plain' | 'circle' | 'square' | 'outlined';
size?: 'sm' | 'md' | 'lg';
}
const {
eyebrow,
headline,
sub,
items = [],
columns = 3,
iconStyle = 'circle',
size = 'md',
} = Astro.props;
---
<section class={`igr__section igr__cols-${columns} igr__size-${size} igr__style-${iconStyle}`}>
{(eyebrow || headline || sub) && (
<div class="igr__header">
{eyebrow && <p class="igr__eyebrow">{eyebrow}</p>}
{headline && <h2 class="igr__headline">{headline}</h2>}
{sub && <p class="igr__sub">{sub}</p>}
</div>
)}
<ul class="igr__grid" role="list">
{items.map((item, i) => (
<li class="igr__item" style={`--delay: ${i * 80}ms`}>
{item.href ? (
<a href={item.href} class="igr__card igr__card--link">
<span class="igr__icon-wrap" aria-hidden="true" set:html={item.icon} />
<span class="igr__title">{item.title}</span>
{item.description && <span class="igr__desc">{item.description}</span>}
</a>
) : (
<div class="igr__card">
<span class="igr__icon-wrap" aria-hidden="true" set:html={item.icon} />
<span class="igr__title">{item.title}</span>
{item.description && <span class="igr__desc">{item.description}</span>}
</div>
)}
</li>
))}
</ul>
</section>
<script>
const grids = document.querySelectorAll<HTMLElement>('.igr__grid');
grids.forEach((grid) => {
const items = grid.querySelectorAll<HTMLElement>('.igr__item');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
(entry.target as HTMLElement).classList.add('igr__item--visible');
observer.unobserve(entry.target);
}
});
},
{ threshold: 0.1, rootMargin: '0px 0px -40px 0px' }
);
items.forEach((item) => observer.observe(item));
});
</script>
<style>
:root {
--color-primary: #0a0a0a;
--color-accent: #6366f1;
--color-bg: #fff;
--color-muted: #6b7280;
--radius: 0.5rem;
}
.igr__section {
padding: 5rem 1.5rem;
max-width: 72rem;
margin-inline: auto;
}
/* Header */
.igr__header {
text-align: center;
margin-bottom: 3rem;
}
.igr__eyebrow {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
margin: 0 0 0.75rem;
}
.igr__headline {
font-size: clamp(1.75rem, 3vw, 2.5rem);
font-weight: 800;
color: var(--color-primary);
margin: 0 0 1rem;
line-height: 1.15;
}
.igr__sub {
font-size: 1.0625rem;
color: var(--color-muted);
max-width: 40rem;
margin-inline: auto;
line-height: 1.65;
margin-top: 0;
margin-bottom: 0;
}
/* Grid */
.igr__grid {
display: grid;
gap: 1.5rem;
list-style: none;
padding: 0;
margin: 0;
}
.igr__cols-2 .igr__grid { grid-template-columns: repeat(2, 1fr); }
.igr__cols-3 .igr__grid { grid-template-columns: repeat(3, 1fr); }
.igr__cols-4 .igr__grid { grid-template-columns: repeat(4, 1fr); }
/* Card */
.igr__card {
display: flex;
flex-direction: column;
gap: 0.875rem;
padding: 1.5rem;
border-radius: var(--radius);
border: 1px solid rgba(0, 0, 0, 0.07);
background: var(--color-bg);
height: 100%;
transition: transform 0.22s ease, box-shadow 0.22s ease;
}
.igr__card--link {
text-decoration: none;
color: inherit;
}
.igr__card:hover {
transform: translateY(-3px);
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.08);
}
/* Icon wrapper */
.igr__icon-wrap {
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
color: var(--color-accent);
}
/* Size: md (default) */
.igr__size-md .igr__icon-wrap { width: 48px; height: 48px; }
.igr__size-sm .igr__icon-wrap { width: 40px; height: 40px; }
.igr__size-lg .igr__icon-wrap { width: 64px; height: 64px; }
.igr__icon-wrap :global(svg) {
width: 55%;
height: 55%;
}
/* iconStyle: plain */
.igr__style-plain .igr__icon-wrap {
background: none;
border: none;
}
/* iconStyle: circle */
.igr__style-circle .igr__icon-wrap {
border-radius: 50%;
background: rgba(99, 102, 241, 0.08);
}
/* iconStyle: square */
.igr__style-square .igr__icon-wrap {
border-radius: var(--radius);
background: rgba(99, 102, 241, 0.08);
}
/* iconStyle: outlined */
.igr__style-outlined .igr__icon-wrap {
border-radius: var(--radius);
border: 1.5px solid rgba(99, 102, 241, 0.35);
background: transparent;
}
/* Text */
.igr__title {
font-size: 1rem;
font-weight: 700;
color: var(--color-primary);
line-height: 1.3;
}
.igr__desc {
font-size: 0.9375rem;
color: var(--color-muted);
line-height: 1.65;
}
/* Animation */
.igr__item {
opacity: 0;
transform: translateY(20px);
}
.igr__item--visible {
animation: igr-fadein 0.5s ease forwards;
animation-delay: var(--delay, 0ms);
}
@keyframes igr-fadein {
to {
opacity: 1;
transform: translateY(0);
}
}
/* Responsive */
@media (max-width: 900px) {
.igr__cols-4 .igr__grid { grid-template-columns: repeat(2, 1fr); }
}
@media (max-width: 600px) {
.igr__cols-2 .igr__grid,
.igr__cols-3 .igr__grid,
.igr__cols-4 .igr__grid {
grid-template-columns: 1fr;
}
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
items * | { icon: string; title: string; description: string; href?: string }[] | — | Grid items |
columns | 2 | 3 | 4 | 3 | Aantal kolommen |
iconStyle | 'plain' | 'circle' | 'square' | 'outlined' | 'circle' | Icoon achtergrond stijl |
size | 'sm' | 'md' | 'lg' | 'md' | Icoon grootte |
* = verplicht