LogoGridWithTitle Social Proof
Logo grid met rijke heading sectie. Above (headline boven grid) of split (headline links, logos rechts).
src/components/social-proof/LogoGridWithTitle.astro
---
interface Logo {
name: string;
src: string;
href?: string;
}
interface Props {
eyebrow?: string;
headline: string;
sub?: string;
logos: Logo[];
columns?: 3 | 4 | 5;
grayscale?: boolean;
layout?: 'above' | 'split';
}
const {
eyebrow,
headline,
sub,
logos,
columns = 4,
grayscale = true,
layout = 'above',
} = Astro.props;
---
<section class={`lgwt__section lgwt__layout-${layout}`}>
<div class="lgwt__head">
{eyebrow && <p class="lgwt__eyebrow">{eyebrow}</p>}
<h2 class="lgwt__headline">{headline}</h2>
{sub && <p class="lgwt__sub">{sub}</p>}
</div>
<ul class={`lgwt__grid lgwt__cols-${columns} ${grayscale ? 'lgwt--grayscale' : ''}`} role="list">
{logos.map((logo) => (
<li class="lgwt__cell lgwt__item">
{logo.href ? (
<a class="lgwt__link" href={logo.href} target="_blank" rel="noopener noreferrer" aria-label={logo.name}>
<img class="lgwt__logo" src={logo.src} alt={logo.name} loading="lazy" />
</a>
) : (
<span class="lgwt__link">
<img class="lgwt__logo" src={logo.src} alt={logo.name} loading="lazy" />
</span>
)}
</li>
))}
</ul>
</section>
<script>
const items = document.querySelectorAll<HTMLElement>('.lgwt__item');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry, i) => {
if (entry.isIntersecting) {
const el = entry.target as HTMLElement;
el.style.transitionDelay = `${i * 60}ms`;
el.classList.add('lgwt--item-visible');
observer.unobserve(el);
}
});
},
{ threshold: 0.1 }
);
items.forEach((el) => observer.observe(el));
</script>
<style>
:root {
--color-primary: #0a0a0a;
--color-accent: #6366f1;
--color-bg: #fff;
--color-muted: #6b7280;
--radius: 0.5rem;
}
.lgwt__section {
padding: 5rem 1.5rem;
max-width: 1200px;
margin: 0 auto;
}
/* Layout: above */
.lgwt__layout-above {
display: flex;
flex-direction: column;
align-items: center;
gap: 3rem;
}
.lgwt__layout-above .lgwt__head {
text-align: center;
max-width: 640px;
}
/* Layout: split */
.lgwt__layout-split {
display: grid;
grid-template-columns: 2fr 3fr;
gap: 4rem;
align-items: center;
}
.lgwt__layout-split .lgwt__head {
text-align: left;
}
.lgwt__eyebrow {
margin: 0 0 0.5rem;
font-size: 0.75rem;
font-weight: 700;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
}
.lgwt__headline {
margin: 0 0 0.75rem;
font-size: clamp(1.75rem, 3vw, 2.5rem);
font-weight: 800;
letter-spacing: -0.02em;
color: var(--color-primary);
line-height: 1.15;
}
.lgwt__sub {
margin: 0;
font-size: 1.0625rem;
line-height: 1.7;
color: var(--color-muted);
}
/* Grid */
.lgwt__grid {
list-style: none;
margin: 0;
padding: 0;
display: grid;
gap: 0;
width: 100%;
}
.lgwt__cols-3 { grid-template-columns: repeat(3, 1fr); }
.lgwt__cols-4 { grid-template-columns: repeat(4, 1fr); }
.lgwt__cols-5 { grid-template-columns: repeat(5, 1fr); }
.lgwt__cell {
display: flex;
align-items: center;
justify-content: center;
padding: 1.25rem 1.5rem;
border: 1px solid rgba(0, 0, 0, 0.06);
margin: -0.5px;
opacity: 0;
transform: translateY(12px);
transition: opacity 0.4s ease, transform 0.4s ease, filter 0.25s ease;
}
.lgwt__cell.lgwt--item-visible {
opacity: 1;
transform: translateY(0);
}
.lgwt__link {
display: flex;
align-items: center;
justify-content: center;
text-decoration: none;
}
.lgwt__logo {
max-height: 40px;
max-width: 160px;
object-fit: contain;
display: block;
transition: filter 0.25s ease;
}
/* Grayscale */
.lgwt--grayscale .lgwt__logo {
filter: grayscale(100%) opacity(0.5);
}
.lgwt--grayscale .lgwt__cell:hover .lgwt__logo {
filter: none;
}
/* Responsive */
@media (max-width: 1024px) {
.lgwt__layout-split {
grid-template-columns: 1fr;
}
.lgwt__layout-split .lgwt__head {
text-align: center;
}
}
@media (max-width: 768px) {
.lgwt__cols-5 { grid-template-columns: repeat(3, 1fr); }
.lgwt__cols-4 { grid-template-columns: repeat(3, 1fr); }
}
@media (max-width: 480px) {
.lgwt__grid { grid-template-columns: repeat(2, 1fr) !important; }
}
@media (prefers-reduced-motion: reduce) {
* {
animation: none !important;
transition: none !important;
}
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
headline * | string | — | Sectie headline |
logos * | { name: string; src: string; href?: string }[] | — | Logo items |
layout | 'above' | 'split' | 'above' | Headline boven of links naast logos |
columns | 3 | 4 | 5 | 4 | Aantal kolommen |
grayscale | boolean | true | Grayscale filter |
* = verplicht