NavMega Navigation
Navigatie met mega-menu dropdown — categorieën, beschrijvingen en featured item.
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