NumberedList list
Gestyled genummerd lijst. 3 varianten: standaard, groot decoratief nummer en inline. Verbindingslijn in stack layout.
src/components/list/NumberedList.astro
---
interface Props {
eyebrow?: string;
headline?: string;
items: {
number?: string;
title: string;
description: string;
image?: string;
}[];
variant?: 'default' | 'large' | 'inline';
layout?: 'stack' | 'grid';
}
const {
eyebrow,
headline,
items = [],
variant = 'default',
layout = 'stack',
} = Astro.props;
function autoNumber(i: number) {
return String(i + 1).padStart(2, '0');
}
---
<section class={`nml__section nml__variant-${variant} nml__layout-${layout}`}>
{(eyebrow || headline) && (
<div class="nml__header">
{eyebrow && <p class="nml__eyebrow">{eyebrow}</p>}
{headline && <h2 class="nml__headline">{headline}</h2>}
</div>
)}
<ol class="nml__list" role="list">
{items.map((item, i) => (
<li class="nml__item" style={`--delay: ${i * 100}ms; --index: ${i}`}>
{variant === 'large' ? (
<div class="nml__large-wrap">
<span class="nml__large-num" aria-hidden="true">{item.number ?? autoNumber(i)}</span>
<div class="nml__text">
<p class="nml__title">{item.title}</p>
<p class="nml__desc">{item.description}</p>
</div>
</div>
) : variant === 'inline' ? (
<div class="nml__inline-wrap">
<p class="nml__inline-heading">
<span class="nml__num-circle">{item.number ?? autoNumber(i)}</span>
<span class="nml__title">{item.title}</span>
</p>
<p class="nml__desc">{item.description}</p>
{item.image && <img src={item.image} alt={item.title} class="nml__img" loading="lazy" />}
</div>
) : (
/* default */
<div class="nml__default-wrap">
<span class="nml__num-circle" aria-hidden="true">{item.number ?? autoNumber(i)}</span>
{layout === 'stack' && i < items.length - 1 && (
<span class="nml__connector" aria-hidden="true" />
)}
<div class="nml__text">
<p class="nml__title">{item.title}</p>
<p class="nml__desc">{item.description}</p>
{item.image && <img src={item.image} alt={item.title} class="nml__img" loading="lazy" />}
</div>
</div>
)}
</li>
))}
</ol>
</section>
<script>
const lists = document.querySelectorAll<HTMLElement>('.nml__list');
lists.forEach((list) => {
const items = list.querySelectorAll<HTMLElement>('.nml__item');
const observer = new IntersectionObserver(
(entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
(entry.target as HTMLElement).classList.add('nml__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;
}
.nml__section {
padding: 5rem 1.5rem;
max-width: 56rem;
margin-inline: auto;
}
/* Header */
.nml__header {
margin-bottom: 3rem;
}
.nml__eyebrow {
font-size: 0.75rem;
font-weight: 600;
letter-spacing: 0.1em;
text-transform: uppercase;
color: var(--color-accent);
margin: 0 0 0.75rem;
}
.nml__headline {
font-size: clamp(1.75rem, 3vw, 2.5rem);
font-weight: 800;
color: var(--color-primary);
margin: 0;
line-height: 1.15;
}
/* List */
.nml__list {
list-style: none;
padding: 0;
margin: 0;
display: flex;
flex-direction: column;
gap: 0;
}
.nml__layout-grid .nml__list {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 2rem;
}
/* Animation */
.nml__item {
opacity: 0;
transform: translateY(18px);
}
.nml__item--visible {
animation: nml-fadein 0.5s ease forwards;
animation-delay: var(--delay, 0ms);
}
@keyframes nml-fadein {
to {
opacity: 1;
transform: translateY(0);
}
}
/* ── DEFAULT variant ── */
.nml__default-wrap {
display: flex;
align-items: flex-start;
gap: 1.25rem;
position: relative;
padding-bottom: 2.5rem;
}
.nml__layout-grid .nml__default-wrap {
flex-direction: column;
padding-bottom: 0;
gap: 1rem;
}
/* Number circle */
.nml__num-circle {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
border-radius: 50%;
background: var(--color-accent);
color: #fff;
font-size: 0.8125rem;
font-weight: 900;
flex-shrink: 0;
font-variant-numeric: tabular-nums;
position: relative;
z-index: 1;
}
/* Connecting line (stack layout only) */
.nml__connector {
position: absolute;
left: 19px;
top: 40px;
bottom: 0;
width: 2px;
background: rgba(99, 102, 241, 0.15);
display: block;
}
.nml__layout-grid .nml__connector {
display: none;
}
/* Text */
.nml__text {
padding-top: 0.25rem;
flex: 1;
}
.nml__title {
font-size: 1.0625rem;
font-weight: 700;
color: var(--color-primary);
margin: 0 0 0.375rem;
line-height: 1.3;
}
.nml__desc {
font-size: 0.9375rem;
color: var(--color-muted);
margin: 0;
line-height: 1.65;
}
.nml__img {
display: block;
margin-top: 1rem;
border-radius: var(--radius);
max-width: 100%;
height: auto;
}
/* ── LARGE variant ── */
.nml__variant-large .nml__item {
padding-bottom: 3.5rem;
}
.nml__variant-large .nml__large-wrap {
position: relative;
padding-left: 1rem;
}
.nml__large-num {
position: absolute;
top: -0.5rem;
left: 0;
font-size: clamp(4rem, 8vw, 7rem);
font-weight: 900;
line-height: 1;
color: var(--color-accent);
opacity: 0.12;
pointer-events: none;
user-select: none;
font-variant-numeric: tabular-nums;
letter-spacing: -0.04em;
}
.nml__variant-large .nml__text {
position: relative;
padding-top: clamp(2.5rem, 4vw, 4rem);
}
.nml__variant-large .nml__title {
font-size: 1.25rem;
font-weight: 800;
}
/* ── INLINE variant ── */
.nml__variant-inline .nml__item {
padding-bottom: 2rem;
}
.nml__inline-wrap {
display: flex;
flex-direction: column;
gap: 0.5rem;
}
.nml__inline-heading {
display: flex;
align-items: center;
gap: 0.75rem;
margin: 0;
}
.nml__variant-inline .nml__title {
font-size: 1.125rem;
font-weight: 700;
margin: 0;
}
.nml__variant-inline .nml__desc {
padding-left: calc(40px + 0.75rem);
}
/* Grid responsive */
@media (max-width: 768px) {
.nml__layout-grid .nml__list {
grid-template-columns: 1fr;
gap: 1.5rem;
}
}
@media (prefers-reduced-motion: reduce) {
* { animation: none !important; transition: none !important; }
}
</style>
Props
| Prop | Type | Default | Beschrijving |
|---|---|---|---|
items * | { title: string; description: string; image?: string }[] | — | Lijst items |
variant | 'default' | 'large' | 'inline' | 'default' | Nummer weergave stijl |
layout | 'stack' | 'grid' | 'stack' | Lay-out richting |
eyebrow | string | — | Label boven sectie |
headline | string | — | Sectie headline |
* = verplicht