Zoeken...  ⌘K GitHub

NavMega Navigation

Navigatie met mega-menu dropdown — categorieën, beschrijvingen en featured item.

/nav-mega
src/components/nav/NavMega.astro
---
/**
 * NavMega
 * Navigatie met mega-menu dropdown — categorieën + featured item.
 * Geschikt voor grotere sites met content-heavy navigatie.
 */
interface NavItem {
  label: string;
  href?: string;
  children?: {
    label: string;
    href: string;
    description?: string;
    icon?: string;
  }[];
  featured?: {
    label: string;
    description: string;
    href: string;
    image?: string;
  };
}

interface Props {
  logo?: string;
  logoText?: string;
  items: NavItem[];
  ctaLabel?: string;
  ctaHref?: string;
  ctaSecondary?: string;
  ctaSecondaryHref?: string;
}

const {
  logo,
  logoText = 'Brand',
  items = [],
  ctaLabel = 'Aan de slag',
  ctaHref = '#',
  ctaSecondary = 'Inloggen',
  ctaSecondaryHref = '#',
} = Astro.props;
---

<header class="nm" data-component="nav-mega">
  <nav class="nm__nav">
    <div class="nm__inner">
      <!-- Logo -->
      <a href="/" class="nm__logo">
        {logo ? <img src={logo} alt={logoText} class="nm__logo-img" /> : (
          <span class="nm__logo-text">{logoText}</span>
        )}
      </a>

      <!-- Menu items -->
      <ul class="nm__menu" role="menubar">
        {items.map((item, i) => (
          <li class:list={['nm__item', { 'nm__item--has-children': item.children }]} role="none">
            {item.children ? (
              <button class="nm__link nm__link--toggle" aria-haspopup="true" aria-expanded="false" data-menu-index={i}>
                {item.label}
                <svg width="12" height="12" viewBox="0 0 12 12" fill="none">
                  <path d="M2 4l4 4 4-4" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/>
                </svg>
              </button>
            ) : (
              <a href={item.href ?? '#'} class="nm__link">{item.label}</a>
            )}

            {item.children && (
              <div class="nm__dropdown" role="menu" aria-label={item.label}>
                <div class="nm__dropdown-inner">
                  <div class="nm__dropdown-links">
                    {item.children.map(child => (
                      <a href={child.href} class="nm__dropdown-item" role="menuitem">
                        {child.icon && (
                          <span class="nm__dropdown-icon" set:html={child.icon} />
                        )}
                        <div class="nm__dropdown-text">
                          <span class="nm__dropdown-label">{child.label}</span>
                          {child.description && (
                            <span class="nm__dropdown-desc">{child.description}</span>
                          )}
                        </div>
                      </a>
                    ))}
                  </div>

                  {item.featured && (
                    <div class="nm__featured">
                      {item.featured.image && (
                        <img src={item.featured.image} alt={item.featured.label} class="nm__featured-img" />
                      )}
                      <div class="nm__featured-body">
                        <p class="nm__featured-label">{item.featured.label}</p>
                        <p class="nm__featured-desc">{item.featured.description}</p>
                        <a href={item.featured.href} class="nm__featured-link">Bekijk →</a>
                      </div>
                    </div>
                  )}
                </div>
              </div>
            )}
          </li>
        ))}
      </ul>

      <!-- Actions -->
      <div class="nm__actions">
        <a href={ctaSecondaryHref} class="nm__action-secondary">{ctaSecondary}</a>
        <a href={ctaHref} class="nm__action-primary">{ctaLabel}</a>
      </div>

      <!-- Mobile hamburger -->
      <button class="nm__hamburger" aria-label="Menu" id="nm-hamburger">
        <span></span><span></span><span></span>
      </button>
    </div>
  </nav>

  <!-- Mobile drawer -->
  <div class="nm__drawer" id="nm-drawer" aria-hidden="true">
    {items.map(item => (
      <div class="nm__drawer-section">
        <p class="nm__drawer-heading">{item.label}</p>
        {item.children ? (
          item.children.map(child => (
            <a href={child.href} class="nm__drawer-link">{child.label}</a>
          ))
        ) : (
          <a href={item.href ?? '#'} class="nm__drawer-link">{item.label}</a>
        )}
      </div>
    ))}
    <div class="nm__drawer-ctas">
      <a href={ctaSecondaryHref} class="nm__action-secondary">{ctaSecondary}</a>
      <a href={ctaHref} class="nm__action-primary" style="display:block;text-align:center">{ctaLabel}</a>
    </div>
  </div>
</header>

<style>
  .nm {
    position: relative;
    z-index: 100;
  }

  .nm__nav {
    background: var(--color-bg, #fff);
    border-bottom: 1px solid rgba(0,0,0,0.07);
  }

  .nm__inner {
    max-width: 1200px;
    margin: 0 auto;
    padding: 0 1.5rem;
    height: 64px;
    display: flex;
    align-items: center;
    gap: 2rem;
  }

  /* Logo */
  .nm__logo { text-decoration: none; flex-shrink: 0; }
  .nm__logo-img { height: 32px; }
  .nm__logo-text {
    font-size: 1.125rem;
    font-weight: 800;
    color: var(--color-primary, #0a0a0a);
    letter-spacing: -0.03em;
  }

  /* Menu */
  .nm__menu {
    display: flex;
    list-style: none;
    gap: 0;
    flex: 1;
  }

  .nm__item {
    position: relative;
  }

  .nm__link {
    display: flex;
    align-items: center;
    gap: 0.35rem;
    padding: 0 1rem;
    height: 64px;
    font-size: 0.9rem;
    font-weight: 500;
    color: var(--color-primary, #0a0a0a);
    text-decoration: none;
    background: none;
    border: none;
    cursor: pointer;
    white-space: nowrap;
    transition: color 0.15s;
  }

  .nm__link:hover { color: var(--color-accent, #6366f1); }
  .nm__link--toggle svg { transition: transform 0.2s; }
  .nm__link--toggle.is-open svg { transform: rotate(180deg); }

  /* Dropdown */
  .nm__dropdown {
    position: absolute;
    top: calc(100% + 8px);
    left: -1rem;
    min-width: 520px;
    background: #fff;
    border: 1px solid rgba(0,0,0,0.07);
    border-radius: calc(var(--radius, 0.5rem) * 1.5);
    box-shadow: 0 20px 40px rgba(0,0,0,0.1);
    padding: 1rem;
    display: none;
    z-index: 200;
  }

  .nm__dropdown.is-open { display: block; }

  .nm__dropdown-inner {
    display: grid;
    grid-template-columns: 1fr auto;
    gap: 1rem;
  }

  .nm__dropdown-links {
    display: flex;
    flex-direction: column;
    gap: 0.25rem;
  }

  .nm__dropdown-item {
    display: flex;
    align-items: flex-start;
    gap: 0.75rem;
    padding: 0.625rem 0.75rem;
    border-radius: var(--radius, 0.5rem);
    text-decoration: none;
    transition: background 0.15s;
  }

  .nm__dropdown-item:hover {
    background: #f5f5f7;
  }

  .nm__dropdown-icon {
    width: 36px;
    height: 36px;
    background: color-mix(in srgb, var(--color-accent, #6366f1) 10%, transparent);
    color: var(--color-accent, #6366f1);
    border-radius: 0.5rem;
    display: flex;
    align-items: center;
    justify-content: center;
    flex-shrink: 0;
    font-size: 1rem;
  }

  .nm__dropdown-label {
    display: block;
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-primary, #0a0a0a);
  }

  .nm__dropdown-desc {
    display: block;
    font-size: 0.8125rem;
    color: var(--color-muted, #6b7280);
    margin-top: 0.125rem;
  }

  /* Featured */
  .nm__featured {
    width: 200px;
    background: #f5f5f7;
    border-radius: var(--radius, 0.5rem);
    overflow: hidden;
    display: flex;
    flex-direction: column;
  }

  .nm__featured-img {
    width: 100%;
    height: 120px;
    object-fit: cover;
  }

  .nm__featured-body { padding: 0.875rem; }
  .nm__featured-label { font-size: 0.875rem; font-weight: 700; color: var(--color-primary, #0a0a0a); margin-bottom: 0.25rem; }
  .nm__featured-desc { font-size: 0.8rem; color: var(--color-muted, #6b7280); margin-bottom: 0.625rem; }
  .nm__featured-link { font-size: 0.8rem; font-weight: 700; color: var(--color-accent, #6366f1); text-decoration: none; }

  /* Actions */
  .nm__actions {
    display: flex;
    align-items: center;
    gap: 0.75rem;
    margin-left: auto;
    flex-shrink: 0;
  }

  .nm__action-secondary {
    font-size: 0.875rem;
    font-weight: 600;
    color: var(--color-primary, #0a0a0a);
    text-decoration: none;
    opacity: 0.7;
    transition: opacity 0.15s;
  }

  .nm__action-secondary:hover { opacity: 1; }

  .nm__action-primary {
    font-size: 0.875rem;
    font-weight: 700;
    background: var(--color-accent, #6366f1);
    color: #fff;
    padding: 0.5rem 1.25rem;
    border-radius: var(--radius, 0.5rem);
    text-decoration: none;
    transition: filter 0.15s;
  }

  .nm__action-primary:hover { filter: brightness(1.1); }

  /* Hamburger */
  .nm__hamburger {
    display: none;
    flex-direction: column;
    gap: 5px;
    background: none;
    border: none;
    cursor: pointer;
    padding: 0.5rem;
    margin-left: auto;
  }

  .nm__hamburger span {
    display: block;
    width: 22px;
    height: 2px;
    background: var(--color-primary, #0a0a0a);
    border-radius: 2px;
  }

  /* Mobile drawer */
  .nm__drawer {
    display: none;
    background: var(--color-bg, #fff);
    border-top: 1px solid rgba(0,0,0,0.07);
    padding: 1.5rem;
    flex-direction: column;
    gap: 1.5rem;
  }

  .nm__drawer.is-open { display: flex; }

  .nm__drawer-heading {
    font-size: 0.75rem;
    font-weight: 700;
    text-transform: uppercase;
    letter-spacing: 0.08em;
    color: var(--color-muted, #6b7280);
    margin-bottom: 0.5rem;
  }

  .nm__drawer-link {
    display: block;
    padding: 0.5rem 0;
    color: var(--color-primary, #0a0a0a);
    text-decoration: none;
    font-size: 0.9375rem;
    font-weight: 500;
    border-bottom: 1px solid rgba(0,0,0,0.05);
  }

  .nm__drawer-ctas {
    display: flex;
    flex-direction: column;
    gap: 0.75rem;
    padding-top: 0.5rem;
  }

  @media (max-width: 900px) {
    .nm__menu, .nm__actions { display: none; }
    .nm__hamburger { display: flex; }
  }
</style>

<script>
  const toggleButtons = document.querySelectorAll('.nm__link--toggle');
  const dropdowns = document.querySelectorAll('.nm__dropdown');

  toggleButtons.forEach((btn) => {
    btn.addEventListener('click', (e) => {
      e.stopPropagation();
      const item = btn.closest('.nm__item');
      const dropdown = item?.querySelector('.nm__dropdown');
      const isOpen = dropdown?.classList.contains('is-open');

      // Close all
      dropdowns.forEach(d => d.classList.remove('is-open'));
      toggleButtons.forEach(b => b.classList.remove('is-open'));

      if (!isOpen && dropdown) {
        dropdown.classList.add('is-open');
        btn.classList.add('is-open');
        btn.setAttribute('aria-expanded', 'true');
      } else {
        btn.setAttribute('aria-expanded', 'false');
      }
    });
  });

  document.addEventListener('click', () => {
    dropdowns.forEach(d => d.classList.remove('is-open'));
    toggleButtons.forEach(b => {
      b.classList.remove('is-open');
      b.setAttribute('aria-expanded', 'false');
    });
  });

  // Mobile
  const hamburger = document.getElementById('nm-hamburger');
  const drawer = document.getElementById('nm-drawer');
  hamburger?.addEventListener('click', () => {
    drawer?.classList.toggle('is-open');
    drawer?.setAttribute('aria-hidden', drawer.classList.contains('is-open') ? 'false' : 'true');
  });
</script>

Props

Prop Type Default Beschrijving
items * NavItem[] Nav items. Children activeren dropdown.
logoText string Logo tekst als geen afbeelding
ctaLabel string Primaire CTA tekst
ctaSecondary string Secundaire link tekst

* = verplicht