Header navigation
Learn how to customize the header navigation menu to match your application's structure.
Header component
The main header is located at app/components/header/MainHeader.vue. It includes:
- Logo with link to home/dashboard
- Desktop navigation menu
- Mobile navigation drawer
- Language switcher
- Theme switcher
- User menu (when authenticated)
Navigation items
Navigation items are defined in the navigationItems array:
<script setup lang="ts">
import { Home, Sparkles, HandCoins, Newspaper, Mail } from 'lucide-vue-next'
const navigationItems = [
{
label: 'Dashboard',
icon: Home,
to: '/dashboard',
requiresAuth: true,
},
{
label: 'AI',
icon: Sparkles,
to: '/ai',
requiresAuth: true,
},
{
label: 'Pricing',
icon: HandCoins,
to: '/pricing',
},
{
label: 'Blog',
icon: Newspaper,
to: '/blog',
},
{
label: 'Support',
icon: Mail,
to: '/support',
requiresAuth: true,
},
]
</script>
Adding a new menu item
To add a new navigation item:
- Import the icon:
import { Settings } from 'lucide-vue-next'
- Add to the array:
const navigationItems = [
// ... existing items
{
label: 'Settings',
icon: Settings,
to: '/settings',
requiresAuth: true, // Only show to authenticated users
},
]
- Create the page:
<script setup>
definePageMeta({
middleware: 'auth',
})
</script>
<template>
<div class="base-container py-8">
<h1 class="text-4xl font-bold">Settings</h1>
</div>
</template>
Removing menu items
Simply remove or comment out items you don't need:
const navigationItems = [
{
label: 'Dashboard',
icon: Home,
to: '/dashboard',
requiresAuth: true,
},
// {
// label: 'AI',
// icon: Sparkles,
// to: '/ai',
// requiresAuth: true,
// }, // Commented out
{
label: 'Pricing',
icon: HandCoins,
to: '/pricing',
},
]
Reordering items
Change the order by rearranging items in the array:
const navigationItems = [
{ label: 'Pricing', icon: HandCoins, to: '/pricing' }, // First
{ label: 'Blog', icon: Newspaper, to: '/blog' }, // Second
{ label: 'Dashboard', icon: Home, to: '/dashboard', requiresAuth: true }, // Third
]
Conditional navigation
Show items only to authenticated users
Use requiresAuth: true:
{
label: 'Dashboard',
to: '/dashboard',
requiresAuth: true, // Only shown when logged in
}
The component filters items based on authentication:
<script setup>
const userStore = useUserStore()
const { isAuthenticated } = storeToRefs(userStore)
const filteredNavigationItems = computed(() =>
navigationItems.filter(item => !item.requiresAuth || isAuthenticated.value)
)
</script>
Show items only to subscribers
Add a subscription check:
{
label: 'Premium',
to: '/premium',
requiresSubscription: true,
requiredPlans: ['pro', 'elite'],
}
Update the filter logic:
<script setup>
const { hasAccess } = useSubscription()
const filteredNavigationItems = computed(() =>
navigationItems.filter(item => {
// Check authentication
if (item.requiresAuth && !isAuthenticated.value) {
return false
}
// Check subscription
if (item.requiresSubscription && !hasAccess(item.requiredPlans)) {
return false
}
return true
})
)
</script>
Show items by role
{
label: 'Admin',
to: '/admin',
requiresAuth: true,
requiredRole: 'admin',
}
Icons
The boilerplate uses Lucide icons:
Using different icons
import { Home, Settings, User, FileText, MessageSquare, CreditCard } from 'lucide-vue-next'
const navigationItems = [
{ label: 'Home', icon: Home, to: '/' },
{ label: 'Settings', icon: Settings, to: '/settings' },
{ label: 'Profile', icon: User, to: '/profile' },
{ label: 'Docs', icon: FileText, to: '/docs' },
{ label: 'Chat', icon: MessageSquare, to: '/chat' },
{ label: 'Billing', icon: CreditCard, to: '/billing' },
]
Browse all Lucide icons: lucide.dev/icons
Using icon components
You can also use @nuxt/icon:
<template>
<Icon name="lucide:home" class="size-4" />
<Icon name="lucide:settings" class="size-4" />
</template>
Dropdown menus
To add a dropdown menu in the header:
<template>
<nav class="flex items-center gap-1">
<!-- Regular menu items -->
<Button v-for="item in regularItems" variant="ghost" as-child>
<NuxtLink :to="item.to">{{ item.label }}</NuxtLink>
</Button>
<!-- Dropdown menu -->
<DropdownMenu>
<DropdownMenuTrigger as-child>
<Button variant="ghost">
More
<Icon name="lucide:chevron-down" class="ml-1 size-4" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem as-child>
<NuxtLink to="/about">About</NuxtLink>
</DropdownMenuItem>
<DropdownMenuItem as-child>
<NuxtLink to="/contact">Contact</NuxtLink>
</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem as-child>
<NuxtLink to="/help">Help</NuxtLink>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</nav>
</template>
Highlighting active links
The header automatically highlights the active menu item:
<script setup>
const route = useRoute()
const isActiveMenuItem = (href: string | null) => {
return href && route.path.includes(href)
}
</script>
<template>
<NuxtLink :to="item.to" :class="isActiveMenuItem(item.to) ? 'underline' : ''">
{{ item.label }}
</NuxtLink>
</template>
Custom active styling
Change the active style:
<NuxtLink
:to="item.to"
:class="[
'px-3 py-2 rounded-md transition-colors',
isActiveMenuItem(item.to) ? 'bg-primary text-primary-foreground' : 'hover:bg-muted',
]"
>
{{ item.label }}
</NuxtLink>
Mobile navigation
The mobile navigation is in a Sheet (side drawer):
<template>
<Sheet v-model:open="isMobileMenuOpen">
<SheetTrigger as-child>
<Button size="icon" variant="outline">
<PanelRight class="size-5" />
</Button>
</SheetTrigger>
<SheetContent side="right">
<nav class="grid gap-4">
<NuxtLink v-for="item in filteredNavigationItems" :key="item.to" :to="item.to" class="flex items-center gap-4">
<component :is="item.icon" class="size-5" />
<span>{{ item.label }}</span>
</NuxtLink>
</nav>
</SheetContent>
</Sheet>
</template>
Customizing mobile menu
Change the side:
<SheetContent side="left"></SheetContent>
Change the width:
<SheetContent side="right" class="w-80"></SheetContent>
Header styling
Sticky header
The header is sticky by default:
<template>
<header class="sticky z-40 top-0 bg-background/80 backdrop-blur-lg border-b">
<!-- Header content -->
</header>
</template>
Transparent header
For a transparent header on the landing page:
<template>
<header class="absolute top-0 left-0 right-0 z-40">
<!-- Header content with light text -->
</header>
</template>
Colored header
<template>
<header class="bg-primary text-primary-foreground">
<!-- Header content -->
</header>
</template>
Logo link behavior
The logo links to different pages based on authentication:
<script setup>
const userStore = useUserStore()
const { isAuthenticated } = storeToRefs(userStore)
const logoTo = computed(() => {
return isAuthenticated.value ? '/dashboard' : '/'
})
</script>
<template>
<NuxtLink :to="logoTo">
<AppLogo />
</NuxtLink>
</template>
Change this behavior:
// Always link to home
const logoTo = computed(() => '/')
// Always link to dashboard (if it exists)
const logoTo = computed(() => '/dashboard')
// Link to pricing if not authenticated
const logoTo = computed(() => {
return isAuthenticated.value ? '/dashboard' : '/pricing'
})
User menu
The user menu appears when authenticated:
<template>
<div v-if="isAuthenticated" class="flex items-center gap-2">
<UserMenu />
</div>
</template>
Customize the user menu in app/components/user/UserMenu.vue.
Call-to-action buttons
Add CTA buttons to the header:
<template>
<header>
<nav class="flex items-center gap-4">
<!-- Navigation items -->
<!-- CTA buttons -->
<div v-if="!isAuthenticated" class="flex items-center gap-2 ml-auto">
<Button variant="ghost" as-child>
<NuxtLink to="/auth/login">Log in</NuxtLink>
</Button>
<Button as-child>
<NuxtLink to="/auth/register">Sign up</NuxtLink>
</Button>
</div>
</nav>
</header>
</template>
Internationalization
The menu labels use i18n:
<script setup>
const { t } = useI18n()
</script>
<template>
<span>{{ t('header.menu.dashboard') }}</span>
</template>
Add translations in i18n/locales/en.json:
{
"header": {
"menu": {
"dashboard": "Dashboard",
"pricing": "Pricing",
"blog": "Blog"
}
}
}
Best practices
Keep it simple
Don't overcrowd the header. Use 5-7 items max:
- Too many items confuse users
- Use dropdowns for additional links
- Move less important links to the footer
Mobile-first
Test on mobile devices:
- Ensure touch targets are large enough (min 44x44px)
- Use readable font sizes
- Test the mobile menu drawer
Accessibility
Ensure keyboard navigation works:
- Use semantic HTML
- Test with Tab key
- Add ARIA labels where needed
<Button aria-label="Open mobile menu">
<PanelRight class="size-5" />
</Button>