npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@xyz/navigation

v2.0.2

Published

Context-aware, type-safe navigation for multi-tenant Nuxt applications

Readme

@xyz/navigation

A flexible, context-aware navigation module for Nuxt 4 with zero performance overhead and zero boilerplate.

npm version License: MIT

Features

  • 🎯 Context-Aware Paths - Dynamic path resolution using templates ({org}, {slug}, {username})
  • 🔐 Role-Based Filtering - Show/hide items based on user roles
  • 🚩 Feature Flags - Conditional navigation based on enabled features
  • Zero Performance Overhead - SSR-safe with reactive computed properties
  • 🌍 Auto Translation - Automatic i18n integration with zero config
  • 🎨 Normalized Structure - Consistent API for all navigation sections
  • Active States - Automatic route matching and active states
  • 🔧 Runtime Functions - Use composables directly in config
  • 📘 Full TypeScript Support - Complete type safety and inference

Installation

pnpm add @xyz/navigation
# or
npm install @xyz/navigation
# or
yarn add @xyz/navigation

Quick Start

1. Install & Enable

// nuxt.config.ts
export default defineNuxtConfig({
  modules: ['@xyz/navigation']  // That's it!
})

2. Create Unified Config

navigation.config.ts - Everything in one place:

export default {
  // Module Options (optional, auto-detected)
  translationResolver: () => useI18n().t,  // Auto-detects $t if using @nuxtjs/i18n
  
  // Navigation Sections
  sections: {
    // Flat section (auto-normalized)
    main: [
      { label: 'nav.dashboard', to: '/app', icon: 'i-lucide-home' },
      { label: 'nav.projects', to: '/projects', icon: 'i-lucide-folder' }
    ],
    
    // Nested section with header
    organization: {
      header: (ctx) => ({
        title: ctx.activeOrganization?.name ?? 'Personal',
        avatar: { 
          // ✅ Use composables in config - functions execute at runtime!
          src: toAbsoluteUrl(ctx.activeOrganization?.logo || ''),
          icon: 'i-lucide-building-2' 
        }
      }),
      items: [
        { label: 'nav.overview', to: '{org}' },
        { label: 'nav.team', to: '{org}/team', features: ['team'] }
      ],
      children: [
        { label: 'nav.settings', to: '{org}/settings' }
      ]
    }
  }
}

3. Use in Components

<script setup>
import navigationConfig from '~/navigation.config'

const { sections } = useNavigation(navigationConfig)
// sections.value = { main: {...}, organization: {...} }
</script>

<template>
  <nav>
    <!-- All sections have normalized structure -->
    <UNavigationMenu :items="sections.value.main.items" />
    
    <!-- Headers with runtime-resolved data -->
    <div v-if="sections.value.organization.header">
      <UAvatar :src="sections.value.organization.header.avatar.src" />
      <h2>{{ sections.value.organization.header.title }}</h2>
    </div>
    
    <!-- Items with auto-computed active states -->
    <UNavigationMenu :items="sections.value.organization.items" />
  </nav>
</template>

Note: Labels are auto-translated if using @nuxtjs/i18n or a custom translationResolver.

Note: Labels are auto-translated if using @nuxtjs/i18n or a custom translationResolver.

Unified Configuration

navigation.config.ts is your single source of truth for both module options and navigation data.

All-in-One Config

// navigation.config.ts
export default {
  // Module Options (build-time, optional)
  translationResolver: () => useTranslations().t,
  contextProvider: () => ({
    user: useAuthUser(),
    activeOrganization: useActiveOrg(),
    features: useFeatureFlags()
  }),
  templates: {
    workspace: (ctx) => ctx.workspaceSlug || 'default'
  },
  
  // Navigation Sections (runtime)
  sections: {
    main: [...],
    organization: {...}
  }
}

Override in nuxt.config.ts

nuxt.config.ts can override individual module options:

export default defineNuxtConfig({
  navigation: {
    translationResolver: () => customResolver()  // Overrides navigation.config.ts
  }
})

Priority: nuxt.config.ts > navigation.config.ts module options > navigation.config.ts sections

Core Concepts

Section Normalization

All sections return a consistent structure:

{
  items?: NavigationMenuItem[]      // Main navigation items
  header?: SectionHeader            // Optional header with title, avatar
  children?: NavigationMenuItem[]   // Sub-navigation items
  footer?: NavigationMenuItem[]     // Footer items
}

Flat sections (arrays) are auto-wrapped:

// Config
sections: {
  main: [{ label: 'Dashboard', to: '/app' }]
}

// Result
sections.value.main = {
  items: [{ label: 'Dashboard', to: '/app', active: true }],
  header: undefined,
  children: [],
  footer: []
}

Runtime Functions in Config

Config functions execute at runtime, so composables work:

export default {
  sections: {
    organization: {
      header: (ctx) => ({
        title: ctx.activeOrganization?.name,
        avatar: {
          // ✅ Use composables directly!
          src: toAbsoluteUrl(ctx.activeOrganization?.logo || '')
        }
      }),
      items: [...]
    }
  }
}

Automatic Translation

Zero config with @nuxtjs/i18n:

// navigation.config.ts - use translation keys
export default {
  sections: {
    main: [
      { label: 'nav.dashboard', to: '/app' },  // Auto-translated!
      { label: 'nav.projects', to: '/projects' }
    ]
  }
}

// Component - translations applied automatically
const { sections } = useNavigation(config)

Custom translation resolver:

// nuxt.config.ts
export default defineNuxtConfig({
  navigation: {
    translationResolver: () => {
      const { t } = useTranslations()
      return t
    }
  }
})

Automatic Active States

Navigation items include active boolean based on current route:

{
  label: 'Blog',
  to: '/blog',
  exact: false,  // Active for /blog and /blog/*
  active: true   // ✅ Auto-computed
}

Use in templates:

<template>
  <NuxtLink
    v-for="item in sections.value.main.items"
    :to="item.to"
    :class="{ 'font-bold': item.active }"
  >
    {{ item.label }}
  </NuxtLink>
</template>

Template Variables

Built-in template variables for dynamic paths:

| Template | Resolves To | Example | |----------|-------------|---------| | {org} | /app (personal) or /app/:slug (organization) | {org}/projects/app/acme/projects | | {slug} | Current organization slug | {slug}acme | | {username} | Current user's name or email | /u/{username}/u/john | | {user.id} | Current user ID | /users/{user.id}/users/123 |

Custom Templates

// nuxt.config.ts
export default defineNuxtConfig({
  navigation: {
    templates: {
      workspace: (ctx) => ctx.workspaceSlug || 'default',
      tenant: (ctx) => ctx.activeTenant?.id || ''
    }
  }
})

// Use in config
{ label: 'Workspace', to: '/w/{workspace}/dashboard' }

Advanced Features

Feature Flags

{
  label: 'Analytics',
  to: '/analytics',
  features: ['analytics']  // Only shown if analytics feature enabled
}

Provide features via context:

const { sections } = useNavigation(config, {
  context: {
    features: { analytics: true, team: false }
  }
})

Role-Based Filtering

{
  label: 'Admin Panel',
  to: '/admin',
  roles: ['admin', 'owner']  // Only for admins/owners
}

Configure role hierarchy:

// nuxt.config.ts
export default defineNuxtConfig({
  navigation: {
    config: {
      roles: {
        hierarchy: ['viewer', 'member', 'admin', 'owner'],
        resolver: (user) => user?.role || 'viewer'
      }
    }
  }
})

Nested Navigation

{
  label: 'Settings',
  to: '/settings',
  children: [
    { label: 'General', to: '/settings/general' },
    { label: 'Billing', to: '/settings/billing', roles: ['admin'] }
  ]
}

Custom Conditions

{
  label: 'Upgrade',
  to: '/upgrade',
  condition: (ctx) => !ctx.user?.isPremium  // Only for free users
}

API Reference

useNavigation(config, options?)

Main composable for navigation.

Parameters:

  • config: NavigationConfig - Navigation configuration object
  • options?: UseNavigationOptions - Optional configuration

Returns:

{
  sections: ComputedRef<Record<string, ProcessedSection>>
  context: ComputedRef<NavigationContext>
  refresh: () => void
}

Options:

interface UseNavigationOptions {
  // Custom context provider
  context?: NavigationContext | (() => NavigationContext)
  
  // Reactive updates (default: true)
  reactive?: boolean
  
  // Custom template resolvers
  templates?: Record<string, (ctx: NavigationContext) => string>
  
  // Override global translation function
  translationFn?: (key: string) => string
}

Navigation Item Config

interface NavigationItemConfig {
  label: string                              // Label (or translation key)
  to?: string                                // Path (supports templates)
  icon?: string                              // Icon name
  exact?: boolean                            // Exact route matching (default: true)
  
  // Filtering
  features?: string[]                        // Required feature flags
  roles?: string[]                           // Required user roles
  condition?: (ctx: NavigationContext) => boolean  // Custom condition
  
  // Nested navigation
  children?: NavigationItemConfig[]
  
  // Custom handlers
  onSelect?: (item: NavigationItemConfig) => void
  
  // Divider
  divider?: boolean
  
  // Custom metadata
  badge?: string | number
  [key: string]: any                         // Additional custom fields
}

Section Header

interface SectionHeader {
  title?: string
  subtitle?: string
  avatar?: {
    src?: string | null
    icon?: string
    fallback?: string
  }
}

Processed Section

All sections return this normalized structure:

interface ProcessedSection {
  items?: NavigationMenuItem[]      // With active states
  header?: SectionHeader
  children?: NavigationMenuItem[]
  footer?: NavigationMenuItem[]
}

Complete Example

// navigation.config.ts
export default {
  sections: {
    // App navigation (flat)
    app: [
      { 
        label: 'nav.dashboard', 
        to: '/app', 
        icon: 'i-lucide-home',
        exact: true
      },
      { 
        label: 'nav.projects', 
        to: '/app/projects', 
        icon: 'i-lucide-folder',
        exact: false  // Active for /app/projects/*
      }
    ],
    
    // Organization navigation (nested)
    organization: {
      header: (ctx) => ({
        title: ctx.activeOrganization?.name ?? 'Personal',
        subtitle: ctx.activeOrganization?.slug,
        avatar: {
          src: useImageUrl().toAbsoluteUrl(
            ctx.activeOrganization?.logo || ''
          ),
          icon: 'i-lucide-building-2',
          fallback: ctx.activeOrganization?.name?.charAt(0)
        }
      }),
      
      items: [
        { 
          label: 'nav.overview', 
          to: '{org}',
          icon: 'i-lucide-layout-dashboard'
        },
        { 
          label: 'nav.team', 
          to: '{org}/team',
          icon: 'i-lucide-users',
          features: ['team']
        },
        {
          label: 'nav.analytics',
          to: '{org}/analytics',
          icon: 'i-lucide-bar-chart',
          features: ['analytics'],
          roles: ['admin', 'owner']
        }
      ],
      
      children: [
        {
          label: 'nav.settings',
          to: '{org}/settings',
          icon: 'i-lucide-settings',
          children: [
            { label: 'nav.settings.general', to: '{org}/settings/general' },
            { label: 'nav.settings.billing', to: '{org}/settings/billing', roles: ['owner'] }
          ]
        }
      ],
      
      footer: [
        { label: 'nav.help', to: '/help', icon: 'i-lucide-help-circle' },
        { label: 'nav.docs', to: '/docs', icon: 'i-lucide-book' }
      ]
    },
    
    // Profile navigation
    profile: [
      { label: 'nav.profile.account', to: '/profile', icon: 'i-lucide-user' },
      { label: 'nav.profile.preferences', to: '/preferences', icon: 'i-lucide-sliders' },
      { divider: true },
      { label: 'nav.profile.logout', icon: 'i-lucide-log-out', onSelect: () => logout() }
    ]
  }
}

Module Configuration

// nuxt.config.ts
export default defineNuxtConfig({
  navigation: {
    // Translation resolver (runtime)
    translationResolver: () => {
      const { t } = useTranslations()
      return t
    },
    
    // Custom templates
    templates: {
      workspace: (ctx) => ctx.workspaceSlug || 'default'
    },
    
    // Context provider
    contextProvider: (nuxtApp) => ({
      user: nuxtApp.$auth?.user,
      activeOrganization: nuxtApp.$org?.active,
      features: nuxtApp.$features?.all()
    }),
    
    // Role configuration
    config: {
      roles: {
        hierarchy: ['viewer', 'member', 'admin', 'owner'],
        resolver: (user) => user?.role || 'viewer'
      }
    }
  }
})

TypeScript

Full type safety with inference:

import type { NavigationConfig, NavigationMenuItem } from '@xyz/navigation'

const config: NavigationConfig = {
  sections: {
    main: [...]  // Fully typed
  }
}

const { sections } = useNavigation(config)
sections.value.main.items  // NavigationMenuItem[]

Migration Guide

From v1.0 to v1.2

All sections now return normalized structure:

// v1.0
sections.value.main  // NavigationMenuItem[]
sections.value.org   // { items, header, children, footer }

// v1.2 (normalized)
sections.value.main.items  // NavigationMenuItem[]
sections.value.org.items   // NavigationMenuItem[]

Both flat and nested sections now have consistent API.

License

MIT

Contributing

Contributions welcome! Please open an issue or PR.