Customizing the Dashboard
The dashboard is the authenticated shell that signed-in users see. It includes a collapsible sidebar, a contextual header, an organization switcher, and a user menu. All of these are customizable.
The dashboard is the authenticated shell that signed-in users see. It includes a collapsible sidebar, a contextual header, an organization switcher, and a user menu. All of these are customizable.
Dashboard Layout Structure
apps/app/src/app/[locale]/(dashboard)/
├── layout.tsx # server layout — sidebar, header, providers
├── page.tsx # dashboard home page
├── settings/ # settings sub-routes
└── _components/
├── app-sidebar.tsx # sidebar navigation
├── site-header.tsx # top header with page title
├── nav-user.tsx # user menu in sidebar footer
├── theme-switcher.tsx # theme toggle sub-menu
├── language-switcher.tsx # locale toggle sub-menu
├── workspace-demo.tsx # removable demo workspace info + records table
├── workspace-record-dialog.tsx # removable create/edit record dialog
├── auth-error-boundary.tsx # catches auth errors
├── convex-error-boundary.tsx # catches Convex errors
└── sign-out-boundary.tsx # provides sign-out actionThe layout assembles these pieces in this order:
<I18nProviderClient locale={locale}>
<SidebarProvider defaultOpen={defaultOpen}>
<ConvexErrorBoundary>
<AuthErrorBoundary>
<SignOutBoundary>
<AppSidebar ... />
<SidebarInset>
<SiteHeader />
<OrganizationEnsure />
{children}
</SidebarInset>
</SignOutBoundary>
</AuthErrorBoundary>
</ConvexErrorBoundary>
</SidebarProvider>
</I18nProviderClient>Modifying the Sidebar Navigation
Navigation items are defined as a static array in app-sidebar.tsx:
const navigationItems = [
{
href: "/",
icon: LayoutDashboard,
labelKey: "dashboard",
match: "exact",
},
{
href: "/settings/organization",
icon: Building2,
labelKey: "organization",
match: "exact",
},
// ...
];Adding a Navigation Item
-
Add an entry to the
navigationItemsarray:{ href: "/analytics", icon: BarChart3, labelKey: "analytics", match: "exact", }, -
Add the label to every locale file under the
navigationnamespace:// locales/en.ts navigation: { analytics: "Analytics", analyticsDescription: "View usage metrics and insights.", // ... }, -
If the page needs a title in the header, add a route entry in
site-header.tsx:const routeTitles = [ // ...existing routes { description: t("analyticsDescription"), path: "/analytics", title: t("analytics"), }, ]; -
Create the corresponding route at
apps/app/src/app/[locale]/(dashboard)/analytics/page.tsx.
Removing a Navigation Item
Delete the entry from navigationItems. The route files can stay or be removed
depending on whether you want the page accessible via direct URL.
Adding External Links
Set match: "external" and provide a full URL. External links render as <a>
tags with target="_blank":
{
href: "https://docs.example.com",
icon: BookOpen,
labelKey: "documentation",
match: "external",
},Modifying the Site Header
The SiteHeader component in site-header.tsx displays the current page's
title and description. It matches the current pathname against a routeTitles
array and falls back to the dashboard title.
To add headers for new routes, add entries to routeTitles following the
existing pattern. Titles and descriptions are pulled from the navigation
i18n namespace.
Modifying the User Menu
The user menu renders in the sidebar footer via NavUser. It includes:
- User avatar, name, and email
- Plan badge (if billing is readable)
- Upgrade prompt (if on the free plan)
- Links to Settings and Documentation
- Theme switcher sub-menu
- Language switcher sub-menu
- Sign out button
Adding a Menu Item
Add a DropdownMenuItem inside the appropriate DropdownMenuGroup:
<DropdownMenuItem asChild>
<Link href="/my-new-page">
<MyIcon />
{t("myNewLabel")}
</Link>
</DropdownMenuItem>Removing the Theme or Language Switcher
Delete the <ThemeMenuSub /> or <LanguageMenuSub /> line from nav-user.tsx.
Customizing the Organization Switcher
The OrganizationSwitcher component renders a dropdown in the sidebar header
showing the user's workspaces. It supports:
- Switching between workspaces
- Creating new workspaces via a dialog
The component is at apps/app/src/components/organization-switcher.tsx and is
rendered inside the sidebar header, hidden when the sidebar is in icon-collapsed
mode.
Sidebar Behavior
Collapsible Mode
The sidebar is configured with collapsible="icon", meaning it can collapse to
show only icons (no labels). This is controlled by the SidebarProvider and
persisted via a sidebar_state cookie.
Users can toggle the sidebar with:
- The
SidebarTriggerhamburger button in the header - The keyboard shortcut
⌘B(orCtrl+Bon Windows/Linux) - The
SidebarRaildrag handle on the sidebar edge
Mobile Behavior
On viewports below 768 px, the sidebar renders as a Sheet (slide-over
drawer) instead of a fixed panel. This is handled automatically by the
Sidebar component using the useIsMobile() hook.
Preloaded Data
The dashboard layout preloads two queries on the server to avoid loading spinners on initial page render:
getUser— the current user's profile.getCurrentPlanSummary— the workspace's billing plan (only if the user's role hasbilling.readpermission).
These are passed as Preloaded payloads to client components, which read them
with usePreloadedAuthQuery. This pattern avoids waterfall fetches and ensures
the sidebar renders with real data on first paint.
Error Boundaries
The dashboard layout wraps content in three error boundaries:
| Boundary | Catches |
|---|---|
ConvexErrorBoundary | Convex runtime errors with retry UI |
AuthErrorBoundary | Authentication errors, redirects to login |
SignOutBoundary | Provides the useSignOut() hook via context |
These boundaries ensure that backend errors show user-friendly messages instead of crashing the entire app.
Loading States
The sidebar renders an AppSidebarSkeleton while user data is loading. This
skeleton matches the geometry of the final sidebar layout to prevent layout
shift during route transitions.
Individual pages should use loading.tsx files for route-level loading
indicators. Import only small skeleton components — not the full page client
component — to keep the loading bundle small.
Quick Recipes
Replace the Logo
Edit packages/ui/src/components/logo.tsx. The Logo component renders an
inline SVG that uses currentColor, so it automatically adapts to the
sidebar's text color. Replace the SVG <path> with your own.
Change the App Name
Set APP_NAME for backend auth/email branding and update
packages/config/src/brand.ts for the Next.js apps. The sidebar header and
page metadata read from the static brandConfig module.
Hide the Plan Badge
In nav-user.tsx, remove the {planSummary && ...} block that renders the
<Badge> next to the user info.
Move the Theme Switcher to the Header
Extract ThemeMenuSub into a standalone toggle button component and render it
inside SiteHeader instead of the user dropdown.
Next Reads
- UI Overview — the shared UI package structure.
- shadcn/ui — adding or customizing UI primitives.
- Internationalization (i18n) — adding navigation labels for new routes.
- Dark Mode — how theme switching is implemented.
Dark Mode
Leave Localhost supports light mode, dark mode, and system-preference detection out of the box. The implementation uses next-themes on the app side and CSS custom properties on the design-token side.
Internationalization (i18n)
Leave Localhost ships with full internationalization support powered by next-international. The product app includes three locales out of the box — English, French, and Spanish — and adding more is straightforward.