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

@consilioweb/admin-nav

v0.11.0

Published

Payload CMS plugin — customizable admin sidebar navigation with drag & drop

Readme

About

@consilioweb/admin-nav — A fully customizable admin sidebar navigation plugin for Payload CMS 3. Drag & drop reordering, per-user preferences stored in the database, 70+ built-in SVG icons, nested sub-items, i18n support (FR/EN with multi-language labels), and a dedicated admin view to customize everything visually.

Overview

@consilioweb/admin-nav replaces the default Payload CMS admin sidebar with a fully customizable navigation system. Each admin user gets their own layout preferences, persisted in the database. The plugin provides a visual editor (/admin/nav-customizer) where users can drag & drop groups and items, toggle visibility, edit labels and icons, create custom entries, and organize nested sub-menus — all without touching code.

Screenshots

| Nav Customizer | Drag & Drop | |:---:|:---:| | Nav Customizer | Drag & Drop |

| Item Editor (with sub-menus) | Group Editor | |:---:|:---:| | Item Editor | Group Editor |

Table of Contents

Features

Drag & Drop Navigation

Powered by @dnd-kit, the navigation supports:

  • Group reordering — drag entire sections up and down
  • Item reordering — move items within or across groups
  • Touch & keyboard support — accessible on all devices
  • Visual drag overlay — see what you're moving in real-time

Per-User Preferences

  • Each admin user has their own navigation layout
  • Preferences are stored in a dedicated Payload collection
  • Changes persist across sessions and devices
  • One-click reset to defaults

Visual Customization

  • Show/hide any group or item
  • Edit labels — rename anything in the sidebar
  • Edit URLs — change where items link to
  • Custom icons — pick from 70+ SVG icons or use color dots
  • Create groups — add new sections to organize your nav
  • Create items — add custom links to any group
  • Nested sub-items — create dropdown sub-menus with their own icons
  • Collapse groups — set groups to start collapsed by default

70+ Built-in Icons

Inline SVG icons (Lucide-compatible, 24x24 viewBox) — zero external dependencies:

  • Navigation — home, layout-dashboard, settings, menu, compass
  • Content — file-text, newspaper, image, tag, calendar
  • Support — ticket, message-square, users, shield-check, mail-search
  • Management — receipt, briefcase, wallet, credit-card, truck
  • Config — database, globe, palette, key, monitor
  • SEO — search, trending-up, bar-chart, link, award
  • Misc — heart, star, bell, zap, gift, rocket, and many more

Internationalization (i18n)

  • UI translations — all plugin strings are translated (French & English included)
  • Multi-language labels — nav item labels and group titles support string | Record<string, string>
  • Toggle in editor — switch between single-language and multi-language mode per item
  • Auto-detection — existing Record<string, string> labels open in multi-lang mode automatically
  • Fallback chain — resolves: exact language → fallback language → first available value
  • Extensible — follows Payload's deepMergeSimple pattern, so you can override any translation key

Auto-Discovery (Zero-Config)

  • No config needed — call adminNavPlugin() with no arguments
  • Collections — grouped by admin.group, hidden collections excluded
  • Globals — added to a "Configuration" group
  • Custom views — detected and added to a "Views" group
  • Smart icons — 40+ slug-to-icon mappings (fallback: box)
  • Fully customizable — use autoDiscoverNav() to generate a base, then modify

Instant Rendering

  • Two-tier cache — module-level (survives SPA navigation) + sessionStorage (survives page reload)
  • No loading flash — nav renders instantly on page transitions
  • Background sync — always fetches fresh data from the server after rendering the cache
  • SSR-safe — module cache is null on the server, matching client initial state (no hydration mismatch)

Admin View

The plugin adds a dedicated view at /admin/nav-customizer with:

  • Full drag & drop interface
  • Group editor modal (title, ID, collapse state)
  • Item editor modal (label, URL, icon, prefix match, sub-items)
  • Icon picker with search and color mode
  • Toast notifications for save/reset feedback
  • Responsive layout

Installation

pnpm add @consilioweb/admin-nav

Or with npm/yarn:

npm install @consilioweb/admin-nav
yarn add @consilioweb/admin-nav

Note: @dnd-kit is bundled into the client bundle — you do not need to install it separately.

Peer Dependencies

| Package | Version | Required | |---------|---------|----------| | payload | ^3.0.0 | Yes | | @payloadcms/next | ^3.0.0 | Optional (admin views) | | @payloadcms/ui | ^3.0.0 | Optional (admin UI) | | @payloadcms/translations | ^3.0.0 | Optional (i18n) | | next | ^14.0.0 \|\| ^15.0.0 | Optional (admin UI) | | react | ^18.0.0 \|\| ^19.0.0 | Optional (admin UI) | | react-dom | ^18.0.0 \|\| ^19.0.0 | Optional (admin UI) |

Quick Start

Add the plugin to your payload.config.ts:

import { buildConfig } from 'payload'
import { adminNavPlugin } from '@consilioweb/admin-nav'

export default buildConfig({
  // ... your existing config
  plugins: [
    adminNavPlugin({
      defaultNav: [
        {
          id: 'content',
          title: 'Content',
          items: [
            { id: 'pages', href: '/admin/collections/pages', label: 'Pages', icon: 'file-text' },
            { id: 'posts', href: '/admin/collections/posts', label: 'Posts', icon: 'newspaper' },
            { id: 'media', href: '/admin/collections/media', label: 'Media', icon: 'image' },
          ],
        },
        {
          id: 'settings',
          title: 'Settings',
          items: [
            { id: 'users', href: '/admin/collections/users', label: 'Users', icon: 'users' },
            { id: 'settings', href: '/admin/globals/settings', label: 'Settings', icon: 'settings' },
          ],
        },
      ],
    }),
  ],
})

That's it. The plugin will automatically:

  1. Add a admin-nav-preferences collection to store per-user layouts
  2. Register API endpoints under /api/admin-nav/
  3. Inject the AdminNav component into the sidebar via beforeNavLinks
  4. Add the Nav Customizer view at /admin/nav-customizer

Important: After installing, run pnpm generate:importmap to register the new components.

Zero-Config Mode

If you don't provide a defaultNav, the plugin auto-discovers your Payload config and builds the navigation automatically:

import { adminNavPlugin } from '@consilioweb/admin-nav'

export default buildConfig({
  plugins: [
    adminNavPlugin(), // No config needed — collections, globals & views are auto-detected
  ],
})

Auto-discovery will:

  • Group collections by their admin.group value (or "Collections" as fallback)
  • Add globals to a "Configuration" group
  • Add custom admin views to a "Views" group
  • Guess icons from 40+ slug mappings (e.g. pagesfile-text, usersusers, mediaimage)
  • Skip hidden collections (admin.hidden: true)

You can also use autoDiscoverNav programmatically to generate a base config, then customize it:

import { adminNavPlugin, autoDiscoverNav } from '@consilioweb/admin-nav'

const baseNav = autoDiscoverNav(config)
// Modify baseNav as needed...

export default buildConfig({
  plugins: [
    adminNavPlugin({ defaultNav: baseNav }),
  ],
})

Configuration

AdminNavPluginConfig

adminNavPlugin({
  defaultNav: [],                           // Optional — auto-discovered if omitted
  afterNav: [],                             // Component paths to render after the nav
  collectionSlug: 'admin-nav-preferences',  // Preferences collection slug
  userCollectionSlug: 'users',              // User collection for the relationship
  endpointBasePath: '/admin-nav',           // API endpoint prefix
  addCustomizerView: true,                  // Add /admin/nav-customizer view
  navComponentPath: undefined,              // Override AdminNav component path
})

| Option | Type | Default | Description | |--------|------|---------|-------------| | defaultNav | NavGroupConfig[] | Auto-découvert | Structure initiale de la sidebar. Si omis, le plugin auto-découvre les collections, globals et vues depuis la config Payload | | afterNav | string[] | [] | Chemins de composants à afficher après la navigation | | collectionSlug | string | 'admin-nav-preferences' | Slug de la collection pour le stockage des préférences | | userCollectionSlug | string | 'users' | Slug de la collection utilisateurs pour la relation | | endpointBasePath | string | '/admin-nav' | Chemin de base pour les endpoints API | | addCustomizerView | boolean | true | Ajouter la vue admin Nav Customizer à /admin/nav-customizer | | navComponentPath | string | '@consilioweb/admin-nav/client#AdminNav' | Surcharger le chemin du composant AdminNav (pour les installations file: ou link:) |

Types

LocalizedString

type LocalizedString = string | Record<string, string>

Labels and titles accept either a plain string or a per-language record. Both are fully backward-compatible.

NavGroupConfig

interface NavGroupConfig {
  id: string                   // Unique group ID
  title: LocalizedString       // Section header label (string or per-language)
  items: NavItemConfig[]       // Items in this group
  visible?: boolean            // Whether the group is visible (default: true)
  defaultCollapsed?: boolean   // Start collapsed (default: false)
}

NavItemConfig

interface NavItemConfig {
  id: string                   // Unique item ID
  href: string                 // Admin URL path
  label: LocalizedString       // Display label (string or per-language)
  icon: string                 // Icon name or '#RRGGBB' for color dot
  matchPrefix?: boolean        // Activate on pathname.startsWith(href)
  children?: NavItemConfig[]   // Nested sub-items
  visible?: boolean            // Whether visible (default: true)
}

NavLayout

interface NavLayout {
  groups: NavGroupConfig[]  // Ordered list of groups
  version: number           // Schema version for future migrations
}

Advanced Example

adminNavPlugin({
  defaultNav: [
    {
      id: 'content',
      title: { fr: 'Contenu', en: 'Content' },  // Multi-language group title
      items: [
        { id: 'pages', href: '/admin/collections/pages', label: 'Pages', icon: 'file-text' },
        { id: 'posts', href: '/admin/collections/posts', label: { fr: 'Articles', en: 'Blog' }, icon: 'newspaper' },
        { id: 'media', href: '/admin/collections/media', label: { fr: 'Médias', en: 'Media' }, icon: 'image' },
      ],
    },
    {
      id: 'seo',
      title: 'SEO',  // Plain string still works
      defaultCollapsed: true,
      items: [
        { id: 'seo-dashboard', href: '/admin/seo', label: 'Dashboard', icon: 'trending-up', matchPrefix: true },
        { id: 'sitemap', href: '/admin/sitemap-audit', label: 'Sitemap', icon: 'globe' },
        { id: 'redirects', href: '/admin/redirects', label: 'Redirects', icon: 'corner-down-right' },
      ],
    },
    {
      id: 'support',
      title: 'Support',
      items: [
        {
          id: 'tickets',
          href: '/admin/collections/tickets',
          label: 'Tickets',
          icon: 'ticket',
          matchPrefix: true,
          children: [
            { id: 'open', href: '/admin/collections/tickets?status=open', label: 'Open', icon: '#EF4444' },
            { id: 'closed', href: '/admin/collections/tickets?status=closed', label: 'Closed', icon: '#22C55E' },
          ],
        },
      ],
    },
    {
      id: 'admin',
      title: 'Administration',
      items: [
        { id: 'users', href: '/admin/collections/users', label: 'Users', icon: 'users' },
        { id: 'nav-customizer', href: '/admin/nav-customizer', label: 'Customize Nav', icon: 'palette' },
      ],
    },
  ],
  // For file: or link: protocol installs, override the component path:
  // navComponentPath: '@/components/admin/AdminNavWrapper#AdminNav',
})

Internationalization (i18n)

Plugin UI Translations

The plugin ships with French and English translations for all UI strings (buttons, labels, toasts, confirmations, etc.). Translations are automatically merged into Payload's i18n system using deepMergeSimple.

All keys are namespaced under plugin-admin-nav:

t('plugin-admin-nav:save')      // "Sauvegarder" (FR) / "Save" (EN)
t('plugin-admin-nav:editItem')  // "Modifier l'item" (FR) / "Edit item" (EN)

To override or add a language, merge your translations in payload.config.ts:

import { buildConfig } from 'payload'

export default buildConfig({
  i18n: {
    translations: {
      de: {
        'plugin-admin-nav': {
          save: 'Speichern',
          cancel: 'Abbrechen',
          // ... override any key
        },
      },
    },
  },
})

Multi-Language Nav Labels

Item labels and group titles accept string | Record<string, string>:

adminNavPlugin({
  defaultNav: [
    {
      id: 'content',
      title: { fr: 'Contenu', en: 'Content' },  // Multi-language title
      items: [
        {
          id: 'pages',
          href: '/admin/collections/pages',
          label: { fr: 'Pages', en: 'Pages' },   // Multi-language label
          icon: 'file-text',
        },
        {
          id: 'posts',
          href: '/admin/collections/posts',
          label: 'Blog',                           // Simple string still works
          icon: 'newspaper',
        },
      ],
    },
  ],
})

The item and group editors include a Multi-lang toggle that lets users switch between single-language and multi-language mode. When a label is already a Record<string, string>, the editor opens in multi-lang mode automatically.

Utilities

import { resolveLabel, isMultiLang } from '@consilioweb/admin-nav'

// Resolve a label to the current language
resolveLabel({ fr: 'Pages', en: 'Pages' }, 'fr')        // 'Pages'
resolveLabel({ fr: 'Contenu', en: 'Content' }, 'en')    // 'Content'
resolveLabel('Simple string', 'fr')                       // 'Simple string'

// Type guard
isMultiLang({ fr: 'Oui', en: 'Yes' })  // true
isMultiLang('plain')                     // false

usePluginTranslation Hook

Type-safe wrapper around Payload's useTranslation that accepts plugin custom keys:

import { usePluginTranslation } from '@consilioweb/admin-nav/client'

function MyComponent() {
  const { t, i18n } = usePluginTranslation()
  return <span>{t('plugin-admin-nav:save')}</span>
}

Built-in Icons

The plugin includes 70+ inline SVG icons with zero external dependencies. All icons use a 24x24 viewBox and are Lucide-compatible.

Use any icon name in NavItemConfig.icon:

{ id: 'pages', href: '/admin/collections/pages', label: 'Pages', icon: 'file-text' }

For color dots instead of icons, use a hex color:

{ id: 'urgent', href: '...', label: 'Urgent', icon: '#EF4444' }

| Category | Icons | |----------|-------| | Navigation | home, layout-dashboard, settings, menu, compass, map, navigation | | Content | file-text, newspaper, image, tag, calendar, bookmark, clipboard | | Support | ticket, message-square, users, shield-check, mail-search, folder-kanban, file-up | | Management | receipt, briefcase, wallet, credit-card, truck, package, shopping-cart | | Config | database, globe, palette, key, monitor, server, cloud | | SEO | search, trending-up, bar-chart, link, award, target, activity | | Misc | heart, star, bell, zap, gift, rocket, flag, coffee, music, camera |

Programmatic Access

import { getIconNames, getIconPath, iconPaths } from '@consilioweb/admin-nav'

// Get all available icon names
const names = getIconNames() // ['home', 'file-text', ...]

// Get the SVG path data for an icon
const path = getIconPath('home') // 'M3 9l9-7 9 7v11a2...'

// Access the full registry
console.log(Object.keys(iconPaths).length) // 70+

API Endpoints

All endpoints are prefixed with the configured endpointBasePath (default: /admin-nav). All endpoints require an authenticated admin user.

| Method | Path | Description | |--------|------|-------------| | GET | /admin-nav/preferences | Get the current user's navigation preferences | | PATCH | /admin-nav/preferences | Save the current user's navigation layout | | DELETE | /admin-nav/preferences | Reset to default navigation | | GET | /admin-nav/default-nav | Get the plugin's default nav config |

useNavPreferences Hook

For custom components that need to interact with nav preferences:

import { useNavPreferences } from '@consilioweb/admin-nav/client'

function MyComponent() {
  const { layout, isLoaded, isSaving, isCustom, save, reset, reload } = useNavPreferences()

  if (!isLoaded) return <p>Loading...</p>

  return (
    <div>
      <p>Groups: {layout.groups.length}</p>
      <p>Custom layout: {isCustom ? 'Yes' : 'No'}</p>
      <button onClick={reset}>Reset to defaults</button>
    </div>
  )
}

| Property | Type | Description | |----------|------|-------------| | layout | NavLayout | Current navigation layout (user's or default) | | isLoaded | boolean | Whether preferences have been fetched | | isSaving | boolean | Whether a save operation is in progress | | isCustom | boolean | Whether the user has customized their nav | | save | (layout: NavLayout) => Promise<void> | Save a new layout | | reset | () => Promise<void> | Reset to default navigation | | reload | () => Promise<void> | Reload preferences from server |

Caching Strategy

The hook uses a two-tier cache for instant rendering with zero flash:

  1. Module-level cache — JavaScript variables at the module scope survive React component re-mounts during SPA navigation. The layout is available immediately in useState(), so subsequent page navigations render the nav instantly with no loading state.

  2. sessionStorage cache — Persists across full page reloads. Read in useEffect (post-hydration) to avoid React hydration mismatch errors.

On the server, module variables are always null, matching the empty initial state on the client — no hydration mismatch. After the first successful fetch, both caches are populated and all subsequent renders are instant.

Components

Client Components

| Component / Hook | Description | |------------------|-------------| | AdminNav | Main sidebar navigation component (injected via beforeNavLinks) | | NavCustomizer | Full drag & drop editor for the navigation layout | | SortableGroup | Draggable group component (used by NavCustomizer) | | SortableItem | Draggable item component (used by NavCustomizer) | | GroupEditor | Modal for editing group properties (with multi-lang toggle) | | NavItemEditor | Modal for editing item properties, sub-items, and multi-lang labels | | IconPicker | Icon selection dropdown with search and color mode | | useNavPreferences | Hook for reading/saving nav preferences | | usePluginTranslation | Type-safe i18n hook with plugin translation keys |

Server Views

| Component | Path | Description | |-----------|------|-------------| | NavCustomizerView | /admin/nav-customizer | Admin view wrapping the NavCustomizer in Payload's DefaultTemplate |

Package Exports

// Main entry — plugin, collection, endpoints, icons, types, i18n utilities
import {
  adminNavPlugin,
  autoDiscoverNav,
  createAdminNavPreferencesCollection,
  createGetPreferencesHandler,
  createSavePreferencesHandler,
  createResetPreferencesHandler,
  getIconNames,
  getIconPath,
  iconPaths,
  resolveLabel,
  isMultiLang,
} from '@consilioweb/admin-nav'
import type {
  NavItemConfig,
  NavGroupConfig,
  NavLayout,
  AdminNavPluginConfig,
  LocalizedString,
} from '@consilioweb/admin-nav'

// Client components — React components for Payload admin UI
import {
  AdminNav,
  NavCustomizer,
  SortableGroup,
  SortableItem,
  GroupEditor,
  NavItemEditor,
  IconPicker,
  useNavPreferences,
  usePluginTranslation,
  resolveLabel,
  isMultiLang,
} from '@consilioweb/admin-nav/client'
import type { PluginAdminNavTranslationKeys } from '@consilioweb/admin-nav/client'

// Server views — admin views wrapped in DefaultTemplate
import { NavCustomizerView } from '@consilioweb/admin-nav/views'

Requirements

  • Node.js >= 18
  • Payload CMS 3.x
  • React 18.x or 19.x (for admin UI components)
  • Database: Any Payload-supported adapter (SQLite, PostgreSQL, MongoDB)

Uninstall

  1. Remove the plugin from your payload.config.ts
  2. Uninstall the package:
pnpm remove @consilioweb/admin-nav
  1. Regenerate the importmap:
pnpm generate:importmap

Data cleanup (optional)

The admin-nav-preferences collection data remains in your database after uninstall. To remove it:

SQLite:

DROP TABLE IF EXISTS admin_nav_preferences;

PostgreSQL:

DROP TABLE IF EXISTS "admin-nav-preferences" CASCADE;

MongoDB:

db.getCollection('admin-nav-preferences').drop()

License

MIT

Author

Made with passion by ConsilioWEB