Menu
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.
@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
| Term | Meaning |
|---|---|
| Menu | A named collection of items (main, dashboard, account, ...). |
| Group | The 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. |
| Source | Anything 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.
| Option | Type | Default | Purpose |
|---|---|---|---|
content | Record<string, boolean | { collection?: string, field?: string }> | none | Enables 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/descriptionKeyare returned untouched. - Content comes from the unlocalized collection unless you pass
{ locale }, as inuseMenu(event, 'main', { locale: 'en' }). runtime/menus.tssources run once per server process, not once per request, so they should depend on runtime config rather than request data. A source must importextendMenuItemexplicitly 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>
}
| Field | Notes |
|---|---|
id | Always derived from the untranslated label, so it is identical in every locale. A duplicate id in one batch logs a warning in development. |
label, description | Same shape as the navigation entries of the base layer's config collection, so content-sourced and module-sourced items match. |
labelKey, descriptionKey | Resolved 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.
| Export | Where it runs | Purpose |
|---|---|---|
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 file | Typed 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) | Anywhere | Typed identity helper for an item. |
parseMenuPath(path), deriveMenuItemId(item) | Anywhere | Path parsing and id derivation. |
Types MenuItem, MenuSource, RegisterMenuSourceOptions and ParsedMenuPath are exported from the same path.
Hooks
| Hook | Where | When | Payload |
|---|---|---|---|
menus:extend | Nuxt (build) | extendMenu registers items | (menu: string, items: MenuItem[]) |
menus:resolved | Nuxt (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:visible | Nuxt app | useMenu resolved a menu's items | (menu: string) |