Modules

Menu

Named, shareable navigation menus - modules contribute items, Nuxt Content can too, consumers read and render them.

Named, shareable navigation menus. A menu (main, dashboard, account, ...) is a named collection of items, filled by one or more sources: Nuxt Content (for example the website navigation), other modules (at build time or dynamically at runtime), or both. The module is presentation-free: it only exposes data through useMenu, and description and icon are optional fields a consumer may render or ignore.

Requires@frigear-nu/module-kit. Optional@nuxt/content and @nuxtjs/i18n. @nuxt/content is required only for menus that enable the content source (a build-time error is thrown otherwise). Other modules declare @frigear-nu/menu as an optional dependency, so it is only needed when the host wants their items rendered somewhere.

Vocabulary

TermMeaning
MenuA named collection of items (main, dashboard, account, ...).
GroupThe part after a menu's first dot: dashboard.more is group more of menu dashboard. The part before any dot-less path is the root group.
SourceAnything that fills a menu: Nuxt Content, extendMenu, or a module's runtime/menus.ts file.

Quick setup

export default defineNuxtConfig({
  modules: ['@frigear-nu/menu'],
})
<script setup lang="ts">
const { data: items } = useMenu('main')
</script>

<template>
  <nav>
    <NuxtLink v-for="item in items" :key="item.id" :to="item.to">
      {{ item.label }}
    </NuxtLink>
  </nav>
</template>

Options

Options live under menus.

OptionTypeDefaultPurpose
contentRecord<string, boolean | { collection?: string, field?: string }>noneEnables the Nuxt Content source per menu. true reads the config collection's navigation field (as defined by the base layer); an object picks another collection / field.
export default defineNuxtConfig({
  menus: {
    content: {
      main: true,
      dashboard: { collection: 'dashboard', field: 'tiles' },
    },
  },
})

With @nuxtjs/i18n configured with locales, content items are localized by the per-locale collection (config becomes config_en), the same convention as the base layer.

Server

useMenu(event: H3Event, path: string): Promise<MenuItem[]>

Auto-imported in server code. It resolves the same merged list as the client composable: Content items first, then items registered with extendMenu, registerMenuSource or extendMenuItem (including from runtime/menus.ts sources), sorted by order. Paths and wildcards work as on the client.

export default defineEventHandler(event => useMenu(event, 'dashboard.*'))

Differences from the client:

  • Labels are not translated: labelKey / descriptionKey are returned untouched.
  • Content comes from the unlocalized collection unless you pass { locale }, as in useMenu(event, 'main', { locale: 'en' }).
  • runtime/menus.ts sources run once per server process, not once per request, so they should depend on runtime config rather than request data. A source must import extendMenuItem explicitly from @frigear-nu/menu/utils.

App

Reading a menu

useMenu is auto-imported and returns the shape of useAsyncData. It re-evaluates when the content locale changes and merges every source: Content items first, then items registered at build time or by a runtime source.

useMenu(path: string): AsyncData<MenuItem[], NuxtError | undefined> // { data, pending, error, refresh }

const { data: items } = useMenu('main') // the root group of `main`
const { data: more } = useMenu('dashboard.more') // group `more` of `dashboard`
const { data: all } = useMenu('dashboard.*') // every group of `dashboard`, flattened
const { data: tree } = useMenu('dashboard.account.*') // `account` and groups nested under it, not siblings

Paths are split on the first dot only: a.b.c is menu a, group b.c. A trailing .* is a wildcard and is only supported when reading; extendMenu and extendMenuItem reject it.

Items are sorted by order ascending. Items without an order come after those with one; ties (including all items without an order) keep registration order.

Items

type MenuItem = {
  id?: string // stable key; default: `to`, else a slug of `label`
  label: string
  labelKey?: string // i18n key; falls back to `label`
  description?: string
  descriptionKey?: string
  icon?: string
  to?: string
  external?: boolean
  tags?: string[]
  order?: number // lower first
  group?: string // set from the registration path; an explicit value wins
  meta?: Record<string, unknown>
}
FieldNotes
idAlways derived from the untranslated label, so it is identical in every locale. A duplicate id in one batch logs a warning in development.
label, descriptionSame shape as the navigation entries of the base layer's config collection, so content-sourced and module-sourced items match.
labelKey, descriptionKeyResolved by useMenu against the active locale and re-evaluated when it changes. Without @nuxtjs/i18n, or when a key has no message, the plain text is used.

Translating menus

Items from code can carry i18n keys. Ship the messages in your module's runtime/i18n/lang/*.json and register them with registerLocales (see Module Kit). Items from the Content source are localized by their per-locale collection instead.

extendMenuItem('account.dropdown', {
  id: 'logout',
  label: 'Logout',
  labelKey: 'account.menu.logout.label',
  description: 'Sign out of your account.',
  descriptionKey: 'account.menu.logout.description',
})

Contributing from a module

Registration order between modules does not matter: items are merged once every module has finished setting up.

Static items, known at build time

import { extendMenu } from '@frigear-nu/menu/utils'

export default defineNuxtModule({
  meta: { name: '@frigear-nu/example' },
  setup(options, nuxt) {
    extendMenu(nuxt, 'dashboard.more', [
      { label: 'Billing', description: 'Manage your subscription', to: '/billing' },
    ])
  },
})

Dynamic items, needing runtime context (for example useRuntimeConfig)

import { createResolver, defineNuxtModule } from '@nuxt/kit'
import { registerMenuSource } from '@frigear-nu/menu/utils'

export default defineNuxtModule({
  meta: { name: '@frigear-nu/example' },
  setup(options, nuxt) {
    const resolver = createResolver(import.meta.url)
    registerMenuSource(nuxt, { file: resolver.resolve('./runtime/menus'), menu: 'dashboard' })
  },
})
// runtime/menus.ts
import { defineMenuSource, extendMenuItem } from '@frigear-nu/menu/utils'

export default defineMenuSource((items) => {
  const config = useRuntimeConfig().public.example

  // Pushed into the `dashboard` menu's root group.
  if (config?.enabled) items.push({ label: 'Example', to: '/example' })

  // Pushed into a named group instead, created if missing.
  extendMenuItem('dashboard.more', { label: 'Security', to: '/security' })
})

extendMenuItem pushes into shared app state, so it only works from code that runs as part of the Nuxt app (such as a source file), not from a module's build-time setup(); use extendMenu there. A module that should not depend on the menu module registers its source with addOptionalMenuSource from Module Kit, which skips the registration when the menu module is not installed (addMenuSource registers it unconditionally).

Utils

@frigear-nu/menu/utils is framework-agnostic.

ExportWhere it runsPurpose
extendMenu(nuxt, path, items)Module setup()Register static items into one exact group.
registerMenuSource(nuxt, { file, menu })Module setup()Register a runtime/menus.ts source; file must be a resolved path.
defineMenuSource(fn)Source fileTyped identity helper. fn(items) receives the menu's root group array.
extendMenuItem(path, items)Nuxt app (auto-imported)Push into a group from a source or app code.
defineMenuItem(item)AnywhereTyped identity helper for an item.
parseMenuPath(path), deriveMenuItemId(item)AnywherePath parsing and id derivation.

Types MenuItem, MenuSource, RegisterMenuSourceOptions and ParsedMenuPath are exported from the same path.

Hooks

HookWhereWhenPayload
menus:extendNuxt (build)extendMenu registers items(menu: string, items: MenuItem[])
menus:resolvedNuxt (build)Once, after every module has set up(menus: Record<string, Record<string, MenuItem[]>>): menu to group to items; static items only, runtime sources and Content items resolve at render time
menus:visibleNuxt appuseMenu resolved a menu's items(menu: string)
Copyright © 2026