Customize Navigation
How to add, reorder, and nest sidebar items — icons, sub-items, translations
Customize Navigation
TL;DR: Edit one file (
src/lib/config/navigation.ts), add translations, done. Sub-items with query params give you filtered views of the same list page.
When You Need This
- You added a new feature and want it in the sidebar
- You want filtered sub-views (e.g., "Active Clients", "Lead Clients" under "Clients")
- You want to reorder or group navigation items
- You want colored dot indicators next to items
The One File That Matters
src/lib/config/navigation.tsEverything starts here. The sidebar, breadcrumbs, and command palette all read from this config.
NavItem Structure
type NavItem = {
label: string // Translation key (layout.sidebar.nav.{label})
href: string // Route path
icon?: ComponentType // Lucide icon
dotColor?: string // Colored dot indicator (e.g., "bg-green-500")
match?: "exact" | "prefix" // Active state matching (default: "prefix")
isExample?: boolean // Marks as example (shows badge in dev mode)
children?: NavItem[] // Sub-items
}Step 1: Add a Simple Navigation Item
// src/lib/config/navigation.ts
import { Package } from "lucide-react"
const yourNavItems: NavItem[] = [
{
label: "orders",
href: "/orders",
icon: Package,
},
]Then include it in getMainNavItems():
export function getMainNavItems(): NavItem[] {
return [
...coreNavItems, // Dashboard
...yourNavItems, // ← Your items
...adminNavItems, // Users, Settings
]
}Step 2: Add Translation
Add the label to both language files:
// messages/en.json
{
"layout": {
"sidebar": {
"nav": {
"orders": "Orders"
}
}
}
}// messages/pl.json
{
"layout": {
"sidebar": {
"nav": {
"orders": "Zamówienia"
}
}
}
}The sidebar renders labels using t("nav.orders") from the layout.sidebar namespace. Breadcrumbs use the same translations automatically.
Adding Sub-Items (Filtered Views)
Sub-items appear as a collapsible section under the parent. They typically link to the same list page with different query parameters — giving users quick access to filtered views.
{
label: "orders",
href: "/orders",
icon: Package,
children: [
{
label: "orders_pending",
href: "/orders?statusCode=pending",
match: "exact", // ← Required for query param matching
dotColor: "bg-yellow-500", // ← Colored dot indicator
},
{
label: "orders_processing",
href: "/orders?statusCode=processing",
match: "exact",
dotColor: "bg-blue-500",
},
{
label: "orders_completed",
href: "/orders?statusCode=completed",
match: "exact",
dotColor: "bg-green-500",
},
],
}Two things to remember:
match: "exact"— without this, every child would highlight when you're on/ordersbecause the default"prefix"matching would match all of them- Translations — each child needs its own key:
{
"layout": {
"sidebar": {
"nav": {
"orders": "Orders",
"orders_pending": "Pending",
"orders_processing": "Processing",
"orders_completed": "Completed"
}
}
}
}How the Page Reads the Filter
Your list page picks up the query parameter automatically:
// src/app/(dashboard)/orders/page.tsx
const { filters } = useDataTableUrlState({
tableId: "orders",
filterKeys: ["statusCode"],
})
const { data } = useQuery(
orpcUtils.orders.list.queryOptions({
input: {
page,
pageSize,
...(filters.statusCode.length > 0 && {
statusCode: filters.statusCode,
}),
},
})
)Click "Pending" in the sidebar → URL becomes /orders?statusCode=pending → DataTable shows only pending orders → sidebar highlights "Pending" sub-item.
Colored Dot Indicators
The dotColor property adds a small colored dot before the label. Use any Tailwind background class:
{
label: "orders_urgent",
href: "/orders?priority=urgent",
match: "exact",
dotColor: "bg-red-500", // Red dot
}This works on both parent and child items. Common colors:
| Color | Class | Use case |
|---|---|---|
| Green | bg-green-500 | Active, completed |
| Yellow | bg-yellow-500 | Pending, warning |
| Blue | bg-blue-500 | In progress |
| Red | bg-red-500 | Urgent, overdue |
| Slate | bg-slate-400 | Draft, inactive |
Active State: How It Works
The sidebar highlights the current page automatically:
match: "prefix"(default) —/ordersmatches/orders,/orders/123,/orders/123/editmatch: "exact"—/orders?statusCode=pendingonly matches that exact path + query params
When to use which:
- Parent items →
"prefix"(default, don't set anything) - Sub-items with query params →
"exact" - Items that share a path prefix →
"exact"to avoid double-highlighting
Collapsed Sidebar Behavior
When the sidebar is collapsed (icon-only mode), sub-items appear in a hover popover:
- Parent shows as an icon
- Hovering opens a popup with the parent label and all children
- Children keep their dot indicators and labels in the popover
- Click any item to navigate
This works automatically — no extra configuration needed.
Icons
Import icons from lucide-react. Some commonly used ones:
import {
Building2, // Companies, organizations
CalendarCheck, // Events, activities, schedules
Contact, // People, contacts
FileText, // Documents, files
LayoutDashboard, // Dashboard
Package, // Orders, products
Receipt, // Invoices
Settings, // Settings
Users, // Team, users
FolderKanban, // Projects
ListTodo, // Tasks
BarChart3, // Reports, analytics
} from "lucide-react"Browse all icons at lucide.dev/icons.
Breadcrumbs
Breadcrumbs are generated automatically from the URL path. They use the same translations as the sidebar:
/orders/abc-123 → Orders > abc-12...- Path segments matching a nav item label → translated name
- UUID-like segments → truncated (
abc-12...) - Other segments → converted from kebab-case to Title Case
No extra configuration needed. If you added the nav item and its translation, breadcrumbs just work.
Complete Example: Adding "Orders" with Sub-Items
1. Navigation config
// src/lib/config/navigation.ts
import { Package } from "lucide-react"
const businessNavItems: NavItem[] = [
{
label: "orders",
href: "/orders",
icon: Package,
children: [
{
label: "orders_pending",
href: "/orders?statusCode=pending",
match: "exact",
dotColor: "bg-yellow-500",
},
{
label: "orders_shipped",
href: "/orders?statusCode=shipped",
match: "exact",
dotColor: "bg-blue-500",
},
{
label: "orders_delivered",
href: "/orders?statusCode=delivered",
match: "exact",
dotColor: "bg-green-500",
},
],
},
]
export function getMainNavItems(): NavItem[] {
return [
...coreNavItems,
...businessNavItems,
...(shouldShowExamplesInMenu() ? exampleNavItems : []),
...adminNavItems,
]
}2. Translations
// messages/en.json — add to layout.sidebar.nav
{
"orders": "Orders",
"orders_pending": "Pending",
"orders_shipped": "Shipped",
"orders_delivered": "Delivered"
}// messages/pl.json — add to layout.sidebar.nav
{
"orders": "Zamówienia",
"orders_pending": "Oczekujące",
"orders_shipped": "Wysłane",
"orders_delivered": "Dostarczone"
}3. Verify
npm run typecheckCommon Mistakes
Don't forget match: "exact" on sub-items:
// ❌ Wrong — all children highlight on /orders
{ label: "orders_pending", href: "/orders?statusCode=pending" }
// ✅ Correct — only highlights on exact match
{ label: "orders_pending", href: "/orders?statusCode=pending", match: "exact" }Don't import getMainNavItems dynamically in components:
// ❌ Wrong — can bundle all icons into the client
import { getMainNavItems } from "@/lib/config/navigation"
// ✅ Correct — import explicitly where needed
import { getMainNavItems } from "@/lib/config/navigation"
// This is fine in layout.tsx or app-sidebar.tsx which already import itDon't skip translations:
// ❌ Wrong — shows raw key "orders_pending" in sidebar
{ label: "orders_pending", href: "/orders?statusCode=pending" }
// Without messages/en.json entry → renders "orders_pending"
// ✅ Correct — add to both en.json and pl.json
// messages/en.json: "orders_pending": "Pending"Related
- Add New Feature — Create the pages that nav items link to
- Add Dictionary — Set up status values for filtered sub-items
- Add Translations — Full i18n guide