Header navigation

Customize the header menu and navigation items

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 are defined in the navigationItems array:

app/components/header/MainHeader.vue
<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:

  1. Import the icon:
import { Settings } from 'lucide-vue-next'
  1. Add to the array:
const navigationItems = [
  // ... existing items
  {
    label: 'Settings',
    icon: Settings,
    to: '/settings',
    requiresAuth: true, // Only show to authenticated users
  },
]
  1. Create the page:
app/pages/settings.vue
<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>

To add a dropdown menu in the header:

app/components/header/MainHeader.vue
<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>

The header automatically highlights the active menu item:

app/components/header/MainHeader.vue
<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):

app/components/header/MainHeader.vue
<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

The header is sticky by default:

app/components/header/MainHeader.vue
<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>

The logo links to different pages based on authentication:

app/components/header/MainHeader.vue
<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:

app/components/header/MainHeader.vue
<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>

Reference

Keep your navigation structure simple and intuitive. Users should be able to find what they're looking for in 2-3 clicks maximum.