@frigear-nu/menu

Named, shareable navigation menus - modules contribute items (extendMenu / a runtime/menus.ts source) and/or Nuxt Content, consumers read them via useMenu and render them however they like.
1,878
Install
npm install @frigear-nu/menu
Latest Version:0.1.0
Vue:3
License:MIT
Last Updated:Oct 15, 2026
Links: npm
Maintainer: hareland

@frigear-nu/menu

Named, shareable navigation menus. A menu (main, dashboard, account, ...) is a named collection of items, filled by one or more sources - Nuxt Content, other modules (at build time, or dynamically at runtime), or both at once (content as the base, modules appending). This package is presentation-free: it only exposes data (useMenu) - description/icon are optional fields a consumer may render however it likes, or ignore.

Quick Setup

Install the module to your Nuxt application with one command:

npx nuxt module add @frigear-nu/menu

Vocabulary

  • Menu - a named collection of items (main, dashboard, account, ...).
  • Group - the part after a menu's first dot. dashboard.more addresses group more of menu dashboard.
  • Source - anything that fills a menu: Nuxt Content, extendMenu, or a module's runtime/menus.ts file.

Contributing items from another module

Static items, known at build time

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

export default defineNuxtModule({
  meta: { name: '@frigear-nu/example' },
  setup(options, nuxt) {
    // `'dashboard.more'` is parsed as menu `dashboard`, group `more` (split on the first dot only).
    extendMenu(nuxt, 'dashboard.more', [
      { label: 'Billing', description: 'Manage your subscription', to: '/billing' },
    ])
  },
})

Dynamic items, needing runtime context (e.g. 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 (in this or another menu) instead - created automatically if it
  // doesn't already exist. `extendMenuItem` pushes into shared app state, so it only works from
  // code that actually runs as part of the Nuxt app (like this file) - not from a module's own
  // build-time `setup()` (use `extendMenu` there instead).
  extendMenuItem('dashboard.more', { label: 'Security', to: '/security' })
})

Registration order between modules doesn't matter for either approach - items are only merged once every module has finished setting up.

Nuxt Content source

Enable the @nuxt/content-backed source for a menu (requires @nuxt/content to be installed):

export default defineNuxtConfig({
  menus: {
    content: {
      // Reads `config.navigation` (the collection/field defined by `@frigear-nu/base`).
      main: true,
      // ...or a custom collection/field:
      dashboard: { collection: 'dashboard', field: 'tiles' },
    },
  },
})

Content items are localized automatically when @nuxtjs/i18n is configured with locales, following the same config -> config_en collection-suffix convention as @frigear-nu/base.

Reading a menu

useMenu is auto-imported. It returns the same shape as useAsyncData ({ data, pending, error, refresh }) - rendering is entirely up to the consumer:

<script setup lang="ts">
const { data: items } = useMenu('main')
const { data: more } = useMenu('dashboard.more')
</script>

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

Items are sorted by order (ascending; items without an order render last, in registration order).

Wildcard reads

A path ending in .* reads every matching group, flattened into one list, instead of a single exact group - useful for rendering a whole menu "tree" at once:

// Every group of the `dashboard` menu (root group included).
const { data: all } = useMenu('dashboard.*')

// `account` and every group nested under it (e.g. `dashboard.account.sub`), but not sibling
// groups like `dashboard.more`.
const { data: accountTree } = useMenu('dashboard.account.*')

Wildcards are only supported when reading a menu - extendMenu/extendMenuItem reject a wildcard path, since items can only ever be registered into one exact group.

Related Packages

@frigear-nu/ui

Shared UI components and composables for Frigear Nuxt applications, built on Nux...

1,005
@frigear-nu/base

Shared Nuxt layer for Frigear websites - bundled modules, UI defaults, content c...

112
@frigear-nu/eslint-config

Shared ESLint flat config for Frigear packages and Nuxt apps.

110
@frigear-nu/notify

Email, SMS and generic notification dispatch (pluggable drivers) Nuxt module, sh...

1,983

Version History

Version Released License
0.1.0 MIT
0.0.11 MIT
0.0.10 MIT
0.0.9 MIT
0.0.8 MIT
Pre-releases (1)
Version Released License
0.0.0-stage