@veiag/payload-enhanced-sidebar
v0.3.2
Published
An enhanced sidebar plugin for Payload CMS with tabbed navigation to organize collections and globals.
Readme
Payload Enhanced Sidebar
An enhanced sidebar plugin for Payload CMS that adds a tabbed navigation system to organize collections and globals into logical groups.
Features
- Tabbed Navigation - Organize collections into separate tabs for cleaner navigation
- Vertical Tab Bar - Icon-based tabs on the left side of the sidebar
- Link Support - Add navigation links (like Dashboard) alongside tabs
- Custom Items - Add custom navigation items that can be merged into existing groups
- Badges - Show notification badges on tabs and navigation items (API-based or reactive provider)
- Custom Components - Replace any part of the sidebar with your own React components
- i18n Support - Full localization support for labels and groups
- Lucide Icons - Use any Lucide icon for tabs and links, or provide a custom icon component per tab

Installation
npm install @veiag/payload-enhanced-sidebar
# or
yarn add @veiag/payload-enhanced-sidebar
# or
pnpm add @veiag/payload-enhanced-sidebarWindows support
See this comment if you have issues with scss : https://github.com/VeiaG/payload-enhanced-sidebar/issues/12#issuecomment-4229207755
Quick Start
import { payloadEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'
import { buildConfig } from 'payload'
export default buildConfig({
// ... your config
plugins: [
payloadEnhancedSidebar({
// Works with defaults!
}),
],
})This will add:
- A Dashboard link at the top
- A default tab showing all collections and globals
- A logout button at the bottom

Configuration
Full Configuration Example
import { payloadEnhancedSidebar } from '@veiag/payload-enhanced-sidebar'
import { buildConfig } from 'payload'
export default buildConfig({
plugins: [
payloadEnhancedSidebar({
// Tabs and links in the sidebar
tabs: [
// Dashboard link
{
id: 'dashboard',
type: 'link',
href: '/',
icon: 'House',
label: { en: 'Dashboard', uk: 'Головна' },
},
// Content tab - shows specific collections
{
id: 'content',
type: 'tab',
icon: 'FileText',
label: { en: 'Content', uk: 'Контент' },
collections: ['posts', 'pages', 'categories'],
},
// Link to external documentation
{
id: 'docs',
type: 'link',
href: 'https://payloadcms.com/',
icon: 'BookOpen',
isExternal: true,
label: { en: 'Documentation', uk: 'Документація' },
},
// E-commerce tab with custom items
{
id: 'ecommerce',
type: 'tab',
icon: 'ShoppingCart',
label: { en: 'E-commerce', uk: 'E-commerce' },
collections: ['products', 'orders', 'customers'],
customItems: [
{
slug: 'analytics',
href: '/analytics',
label: { en: 'Analytics', uk: 'Аналітика' },
group: 'E-commerce', // Merge into existing group
},
{
slug: 'quick-add',
href: '/quick-add',
label: { en: 'Quick Add', uk: 'Швидке додавання' },
position: 'top', // Appears above all collection groups
},
],
},
// Settings tab with globals
{
id: 'settings',
type: 'tab',
icon: 'Settings',
label: { en: 'Settings', uk: 'Налаштування' },
collections: ['users'],
globals: ['site-settings', 'footer-settings'],
customItems: [
{
slug: 'api-keys',
href: '/api-keys',
label: { en: 'API Keys', uk: 'API Ключі' },
// No group - will appear at the bottom
},
{
slug:'external-link',
href: 'https://example.com',
isExternal: true,
label: { en: 'External Link', uk: 'Зовнішнє Посилання'}
}
],
},
],
// Show/hide logout button (default: true)
showLogout: true,
// Disable the plugin
disabled: false,
}),
],
})Configuration Options
tabs
Array of tabs and links to show in the sidebar.
Tab (type: 'tab')
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Unique identifier |
| type | 'tab' | Yes | Tab type |
| icon | IconName | Yes* | Lucide icon name |
| iconComponent | SidebarComponent | Yes* | Path to a custom icon component (string or { path, clientProps }) |
| label | LocalizedString | Yes | Tab tooltip/label |
| collections | CollectionSlug[] | No | Collections to show in this tab |
| globals | GlobalSlug[] | No | Globals to show in this tab |
| customItems | SidebarTabItem[] | No | Custom navigation items (see below) |
| badge | BadgeConfig | No | Badge configuration for the tab icon |
| access | TabAccessFunction | No | Server-side access control — return false to hide |
* Exactly one of
iconoriconComponentis required — they are mutually exclusive. If neithercollectionsnorglobalsare specified, the tab shows all collections and globals.
Link (type: 'link')
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Unique identifier |
| type | 'link' | Yes | Link type |
| icon | IconName | Yes* | Lucide icon name |
| iconComponent | SidebarComponent | Yes* | Path to a custom icon component (string or { path, clientProps }) |
| label | LocalizedString | Yes | Link tooltip/label |
| href | string | Yes | URL |
| isExternal | boolean | No | If true, href is absolute URL, if not, href is relative to admin route |
| badge | BadgeConfig | No | Badge configuration for the link icon |
| access | TabAccessFunction | No | Server-side access control — return false to hide |
* Exactly one of
iconoriconComponentis required — they are mutually exclusive.
Custom slot (type: 'custom')
Renders an arbitrary component in the tabs bar — useful for spacers, separators, decorative elements, etc. Does not open any navigation content.
| Property | Type | Required | Description |
|----------|------|----------|-------------|
| id | string | Yes | Unique identifier |
| type | 'custom' | Yes | Custom slot type |
| component | SidebarComponent | Yes | Component to render (string path or { path, clientProps }) |
| access | TabAccessFunction | No | Server-side access control — return false to hide |
The component receives { id } plus any clientProps you pass. See Custom Components for details.
{
id: 'separator',
type: 'custom',
component: './components/Sidebar#TabSeparator',
}
customItems
Custom items can be added to any tab:
{
slug: 'unique-slug', // Required: unique identifier
href: '/path', // Required: URL
label: { en: 'Label' }, // Required: display label
group: { en: 'Group Name' }, // Optional: merge into existing group or create new
isExternal: true, // Optional: if true, href is absolute URL
position: 'top', // Optional: 'top' | 'bottom' (default: 'bottom')
}Group behavior:
- If
groupmatches an existing collection group label, the item is added to that group - If
groupdoesn't match any existing group, a new group is created - If
groupis not specified, the item appears as ungrouped
Position behavior:
position: 'top'— item (or new custom group) appears above all collection/global groupsposition: 'bottom'— appears below all groups (default)- Has no effect on items that merge into an existing collection group via
group
Badges
Badges allow you to show notification counts on tabs and navigation items. There are three ways to configure badges:
Badge on Tabs/Links
Add a badge property to any tab or link in the tabs array:
tabs: [
{
id: 'orders',
type: 'tab',
icon: 'ShoppingCart',
label: 'Orders',
collections: ['orders'],
// Badge on the tab icon
badge: {
type: 'collection-count',
collectionSlug: 'orders',
color: 'error',
},
},
]Badges on Navigation Items
Use the badges configuration to add badges to any sidebar item (collections, globals, or custom items):
payloadEnhancedSidebar({
badges: {
// Show document count for posts collection
posts: { type: 'collection-count', color: 'primary' },
// Custom API endpoint
orders: {
type: 'api',
endpoint: '/api/orders/pending',
responseKey: 'count',
color: 'error',
},
// Provider-based (reactive)
notifications: { type: 'provider', color: 'warning' },
},
})Badge Types
collection-count
Automatically fetches document count from a collection.
{
type: 'collection-count',
collectionSlug?: string, // Defaults to item's slug
color?: BadgeColor, // 'default' | 'primary' | 'success' | 'warning' | 'error'
where?: object, // Optional filter query
}api
Fetches badge value from a custom API endpoint.
{
type: 'api',
endpoint: string, // API URL (relative or absolute)
method?: 'GET' | 'POST', // Default: 'GET'
responseKey?: string, // Key to extract from response. Default: 'count'
color?: BadgeColor,
}provider
Uses reactive values from BadgeProvider context. Values update automatically when the provider changes.
{
type: 'provider',
slug?: string, // Key in provider values. Defaults to item's slug/id
color?: BadgeColor,
}Using BadgeProvider
For reactive badges (real-time updates, websockets, etc.), use the BadgeProvider:
- Create a provider component:
// components/MyBadgeProvider.tsx
'use client'
import { BadgeProvider } from '@veiag/payload-enhanced-sidebar'
import { useEffect, useState } from 'react'
export const MyBadgeProvider = ({ children }) => {
const [counts, setCounts] = useState({
orders: 0,
notifications: 0,
})
useEffect(() => {
// Fetch initial counts, subscribe to websocket, etc.
const ws = new WebSocket('wss://your-api/counts')
ws.onmessage = (e) => setCounts(JSON.parse(e.data))
return () => ws.close()
}, [])
return <BadgeProvider values={counts}>{children}</BadgeProvider>
}- Add it to Payload's providers:
// payload.config.ts
export default buildConfig({
admin: {
components: {
providers: ['./components/MyBadgeProvider#MyBadgeProvider'],
},
},
})- Configure badges to use the provider:
payloadEnhancedSidebar({
badges: {
orders: { type: 'provider', color: 'error' },
},
tabs: [
{
id: 'notifications',
type: 'link',
href: '/notifications',
icon: 'Bell',
label: 'Notifications',
badge: { type: 'provider', slug: 'notifications', color: 'warning' },
},
],
})Badge Colors
Available colors: default, primary, success, warning, error

Badge Display
- Numbers up to 99 are shown as-is
- Numbers > 99 are shown as "99+"
- Zero or undefined values hide the badge
- Provider values can also be React nodes for custom rendering
Access Control
You can control visibility of tabs, links, and custom items using an access function. It runs server-side and receives the current PayloadRequest, so you have full access to req.user, roles, permissions, etc.
On tabs and links
tabs: [
{
id: 'admin-panel',
type: 'tab',
icon: 'Shield',
label: 'Admin',
collections: ['users', 'tenants'],
access: ({ req, item }) => {
return req.user?.role === 'admin'
},
},
{
id: 'reports',
type: 'link',
href: '/reports',
icon: 'BarChart',
label: 'Reports',
access: async ({ req }) => {
// async is supported
return Boolean(req.user)
},
},
]If access returns false, the tab button is hidden from the tabs bar and its content is not rendered.
On custom items
customItems: [
{
slug: 'admin-tools',
href: '/admin-tools',
label: 'Admin Tools',
access: ({ req }) => req.user?.role === 'admin',
},
]Access function signatures:
// For tabs and links
type TabAccessFunction = (args: {
item: SidebarTab // full tab/link config
req: PayloadRequest
}) => boolean | Promise<boolean>
// For custom items
type ItemAccessFunction = (args: {
item: SidebarTabItem // full custom item config
req: PayloadRequest
}) => boolean | Promise<boolean>Default collections and globals already respect Payload's built-in access control — they are filtered by
visibleEntitiesautomatically. Theaccessfunction is only needed for tabs, links, and custom items.
Behavior when req is unavailable
Access functions are fail-closed: if req is not available (e.g. on certain error pages), all items with an access function will be hidden. This is a known limitation caused by a Payload bug where req is not passed to the Nav component on 404 admin pages.
Custom views and access control
If you have custom admin views, you must pass req to DefaultTemplate for access control to work correctly. Retrieve it from props.initPageResult.req:
import type { AdminViewProps } from 'payload'
import { DefaultTemplate } from '@payloadcms/next/templates'
export async function MyCustomView(props: AdminViewProps) {
const { initPageResult, params, searchParams } = props
const { permissions, req, visibleEntities } = initPageResult
const { i18n, locale, payload, user } = req
return (
<DefaultTemplate
i18n={i18n}
locale={locale}
params={params}
payload={payload}
permissions={permissions}
req={req}
searchParams={searchParams}
user={user ?? undefined}
visibleEntities={visibleEntities}
>
{/* your view content */}
</DefaultTemplate>
)
}Without req={req}, the sidebar will treat the page as unauthenticated and hide all access-controlled items.
showLogout
Show/hide the logout button at the bottom of the tabs bar.
- Type:
boolean - Default:
true
disabled
Completely disable the plugin.
- Type:
boolean - Default:
false
Custom Components
You can replace any part of the sidebar with your own React components. The plugin registers them automatically in Payload's import map — no manual import map configuration needed.
payloadEnhancedSidebar({
customComponents: {
// Replace individual nav items (collections, globals, custom links)
NavItem: './components/Sidebar#MyNavItem',
// Replace group headers
NavGroup: './components/Sidebar#MyNavGroup',
// Replace the entire nav scroll area
NavContent: './components/Sidebar#MyNavContent',
// Replace every button in the tabs bar (tabs and links)
TabButton: './components/Sidebar#MyTabButton',
},
tabs: [
{
id: 'dashboard',
type: 'link',
href: '/',
// Custom icon for just this tab/link (mutually exclusive with `icon`)
iconComponent: './components/Sidebar#DashboardIcon',
label: 'Dashboard',
},
],
})All custom components are client components ('use client'). The plugin provides hooks to connect them to sidebar state:
| Hook | Description |
|------|-------------|
| useNavItemState(href) | { isActive, isCurrentPage } — for custom NavItem |
| useTabState(id) | { isActive } — for custom NavContent or TabButton |
| useEnhancedSidebar() | { activeTabId, onTabChange } — full tab context |
→ See docs/custom-components.md for full documentation, prop types, and examples for each slot.
Localization
All labels support localized strings:
label: 'Simple string'
// or
label: {
en: 'English',
uk: 'Українська',
de: 'Deutsch',
}Payload Features Support
- Browse by Folder Button - Automatically shows folder view button when Payload folders are enabled (requires Payload v3.41.0+)
- Settings Menu Items - Integrates with Payload's SettingsMenu components (requires Payload v3.60.0+)
beforeNav/afterNavslots - Supports Payload'sadmin.components.beforeNavandadmin.components.afterNavslots (requires Payload v3.75.0+). Both slots are rendered inside the nav content area —beforeNavbeforebeforeNavLinks,afterNavafterafterNavLinks.
Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
- Fork the repository
- Create your feature branch (
git checkout -b feature/amazing-feature) - Commit your changes (
git commit -m 'Add some amazing feature') - Push to the branch (
git push origin feature/amazing-feature) - Open a Pull Request
Issues
Found a bug or have a feature request? Please open an issue on GitHub.
License
MIT © VeiaG
