Leave Localhost logoLeave LocalhostDocs
UI

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 action

The 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

  1. Add an entry to the navigationItems array:

    {
      href: "/analytics",
      icon: BarChart3,
      labelKey: "analytics",
      match: "exact",
    },
  2. Add the label to every locale file under the navigation namespace:

    // locales/en.ts
    navigation: {
      analytics: "Analytics",
      analyticsDescription: "View usage metrics and insights.",
      // ...
    },
  3. 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"),
      },
    ];
  4. 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.

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.

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 SidebarTrigger hamburger button in the header
  • The keyboard shortcut ⌘B (or Ctrl+B on Windows/Linux)
  • The SidebarRail drag 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 has billing.read permission).

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:

BoundaryCatches
ConvexErrorBoundaryConvex runtime errors with retry UI
AuthErrorBoundaryAuthentication errors, redirects to login
SignOutBoundaryProvides 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

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

On this page