Zoeken...  ⌘K GitHub

IconList icon

Verticale lijst met icoon + titel + beschrijving. Optionele scheidingslijnen, badges en compact mode.

/icon-list
src/components/icon/IconList.astro
---
interface Props {
  eyebrow?: string;
  headline?: string;
  items: {
    icon: string;
    title: string;
    description?: string;
    badge?: string;
  }[];
  divided?: boolean;
  compact?: boolean;
}

const {
  eyebrow,
  headline,
  items = [],
  divided = false,
  compact = false,
} = Astro.props;
---

<section class={`ils__section${compact ? ' ils__compact' : ''}`}>
  {(eyebrow || headline) && (
    <div class="ils__header">
      {eyebrow && <p class="ils__eyebrow">{eyebrow}</p>}
      {headline && <h2 class="ils__headline">{headline}</h2>}
    </div>
  )}

  <ul class={`ils__list${divided ? ' ils__list--divided' : ''}`} role="list">
    {items.map((item, i) => (
      <li class="ils__item" style={`--delay: ${i * 70}ms`}>
        <span class="ils__icon-wrap" aria-hidden="true" set:html={item.icon} />
        <span class="ils__content">
          <span class="ils__title-row">
            <span class="ils__title">{item.title}</span>
            {item.badge && <span class="ils__badge">{item.badge}</span>}
          </span>
          {item.description && <span class="ils__desc">{item.description}</span>}
        </span>
      </li>
    ))}
  </ul>
</section>

<script>
  const lists = document.querySelectorAll<HTMLElement>('.ils__list');

  lists.forEach((list) => {
    const items = list.querySelectorAll<HTMLElement>('.ils__item');

    const observer = new IntersectionObserver(
      (entries) => {
        entries.forEach((entry) => {
          if (entry.isIntersecting) {
            (entry.target as HTMLElement).classList.add('ils__item--visible');
            observer.unobserve(entry.target);
          }
        });
      },
      { threshold: 0.1, rootMargin: '0px 0px -30px 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;
  }

  .ils__section {
    padding: 4rem 1.5rem;
    max-width: 48rem;
    margin-inline: auto;
  }

  /* Header */
  .ils__header {
    margin-bottom: 2rem;
  }

  .ils__eyebrow {
    font-size: 0.75rem;
    font-weight: 600;
    letter-spacing: 0.1em;
    text-transform: uppercase;
    color: var(--color-accent);
    margin: 0 0 0.5rem;
  }

  .ils__headline {
    font-size: clamp(1.5rem, 2.5vw, 2rem);
    font-weight: 800;
    color: var(--color-primary);
    margin: 0;
    line-height: 1.2;
  }

  /* List */
  .ils__list {
    list-style: none;
    padding: 0;
    margin: 0;
  }

  .ils__list--divided .ils__item:not(:last-child) {
    border-bottom: 1px solid rgba(0, 0, 0, 0.08);
  }

  /* Item */
  .ils__item {
    display: flex;
    align-items: flex-start;
    gap: 1rem;
    padding: 1.125rem 0;
    opacity: 0;
    transform: translateX(-18px);
  }

  .ils__compact .ils__item {
    padding: 0.625rem 0;
    gap: 0.75rem;
  }

  .ils__item--visible {
    animation: ils-slidein 0.45s ease forwards;
    animation-delay: var(--delay, 0ms);
  }

  @keyframes ils-slidein {
    to {
      opacity: 1;
      transform: translateX(0);
    }
  }

  /* Icon wrapper */
  .ils__icon-wrap {
    display: flex;
    align-items: center;
    justify-content: center;
    width: 40px;
    height: 40px;
    flex-shrink: 0;
    border-radius: var(--radius);
    background: rgba(99, 102, 241, 0.07);
    color: var(--color-accent);
    transition: background 0.2s ease;
  }

  .ils__compact .ils__icon-wrap {
    width: 32px;
    height: 32px;
  }

  .ils__item:hover .ils__icon-wrap {
    background: rgba(99, 102, 241, 0.16);
  }

  .ils__icon-wrap :global(svg) {
    width: 55%;
    height: 55%;
  }

  /* Content */
  .ils__content {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
    padding-top: 0.125rem;
  }

  .ils__title-row {
    display: flex;
    align-items: center;
    gap: 0.5rem;
    flex-wrap: wrap;
  }

  .ils__title {
    font-size: 1rem;
    font-weight: 700;
    color: var(--color-primary);
    line-height: 1.3;
  }

  .ils__compact .ils__title {
    font-size: 0.9375rem;
  }

  .ils__desc {
    font-size: 0.9rem;
    color: var(--color-muted);
    line-height: 1.65;
  }

  .ils__compact .ils__desc {
    font-size: 0.8125rem;
  }

  /* Badge */
  .ils__badge {
    display: inline-flex;
    align-items: center;
    padding: 0.15em 0.55em;
    border-radius: 999px;
    background: rgba(99, 102, 241, 0.1);
    color: var(--color-accent);
    font-size: 0.7rem;
    font-weight: 600;
    letter-spacing: 0.04em;
    text-transform: uppercase;
    white-space: nowrap;
  }

  @media (prefers-reduced-motion: reduce) {
    * { animation: none !important; transition: none !important; }
  }
</style>

Props

Prop Type Default Beschrijving
items * { icon: string; title: string; description?: string; badge?: string }[] Lijst items
divided boolean false Scheidingslijnen tussen items
compact boolean false Kleinere padding en tekst

* = verplicht