Tulip Logo IconTulip
App

Sidebar

Compose an admin sidebar from explicit header, content, footer, navigation, and account-menu primitives.

The sidebar is composed from layout slots and path navigation primitives. The shell owns placement and collapse behavior; your application decides what belongs in the header, content, and footer.

Import sidebar components from @tulip-systems/app/components/client.

import {
  Path,
  PathGroup,
  PathGroupItems,
  PathLink,
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarRail,
} from "@tulip-systems/app/components/client";

Provider

Sidebar and its navigation primitives must render inside SidebarProvider. For an admin area, use AdminLayout; it provides the sidebar state and pairs the sidebar with AdminContent.

import { AdminContent } from "@tulip-systems/app/components/client";
import { AdminLayout } from "@tulip-systems/app/components/server";
import { AdminSidebar } from "@/lib/config/admin-sidebar";

export default function AdminLayoutRoute(props: { children: React.ReactNode }) {
  return (
    <AdminLayout>
      <AdminSidebar />
      <AdminContent>{props.children}</AdminContent>
    </AdminLayout>
  );
}

If the sidebar is used outside an admin layout, wrap it in SidebarProvider directly.

import { Sidebar, SidebarProvider } from "@tulip-systems/app/components/client";

export function AppSidebar() {
  return (
    <SidebarProvider>
      <Sidebar>{/* header, content, footer, and rail */}</Sidebar>
    </SidebarProvider>
  );
}

Basic sidebar

The standard composition has four slots:

  • SidebarHeader: static or account-related content at the top.
  • SidebarContent: the scrollable navigation area.
  • SidebarFooter: optional persistent actions at the bottom.
  • SidebarRail: optional desktop collapse affordance.
import { LayoutDashboardIcon, SettingsIcon } from "lucide-react";
import {
  DefaultSidebarAccountMenu,
  Path,
  PathGroup,
  PathGroupItems,
  PathLink,
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarHeader,
  SidebarMenu,
  SidebarRail,
} from "@tulip-systems/app/components/client";

export function AdminSidebar() {
  return (
    <Sidebar>
      <SidebarHeader>
        <DefaultSidebarAccountMenu />
      </SidebarHeader>

      <SidebarContent>
        <PathGroup>
          <PathGroupItems>
            <Path segment="(dashboard)">
              <PathLink href="/admin">
                <LayoutDashboardIcon />
                Dashboard
              </PathLink>
            </Path>
          </PathGroupItems>
        </PathGroup>
      </SidebarContent>

      <SidebarFooter>
        <SidebarMenu>
          <Path segment="settings">
            <PathLink href="/admin/settings">
              <SettingsIcon />
              Settings
            </PathLink>
          </Path>
        </SidebarMenu>
      </SidebarFooter>

      <SidebarRail />
    </Sidebar>
  );
}

SidebarRail is deliberately explicit. Omit it when the sidebar should not have a clickable desktop collapse rail, such as when a top-bar trigger is the only collapse control.

Path navigation

Use PathGroup and PathGroupItems to create a top-level menu list. A Path receives its active state from the first selected Next.js layout segment. Its PathLink automatically closes the sidebar after navigation on mobile.

import { ListTodoIcon, Table2Icon } from "lucide-react";
import {
  Path,
  PathGroup,
  PathGroupItems,
  PathGroupTitle,
  PathLink,
} from "@tulip-systems/app/components/client";

<PathGroup permission={[{ project: ["view"] }, { task: ["view"] }]}>
  <PathGroupTitle>Projects</PathGroupTitle>

  <PathGroupItems>
    <Path permission={{ project: ["view"] }} segment="projects">
      <PathLink href="/admin/projects">
        <Table2Icon />
        Projects
      </PathLink>
    </Path>

    <Path permission={{ task: ["view"] }} segment="tasks">
      <PathLink href="/admin/tasks">
        <ListTodoIcon />
        My Tasks
      </PathLink>
    </Path>
  </PathGroupItems>
</PathGroup>

PathGroup.permission uses an any strategy, so the group appears when the user has at least one permission. Path.permission hides an individual item when its permission is unavailable.

Nested paths

Put PathItems inside a Path for second-level navigation. It only renders while its parent path is active. PathSubItem matches the second selected layout segment.

import { Table2Icon } from "lucide-react";
import {
  Path,
  PathItems,
  PathLink,
  PathSubItem,
  PathSubItemLink,
} from "@tulip-systems/app/components/client";

<Path permission={{ project: ["view"] }} segment="projects">
  <PathLink href="/admin/projects">
    <Table2Icon />
    Projects
  </PathLink>

  <PathItems>
    <PathSubItem permission={{ project: ["view.drive"] }} segment="drive">
      <PathSubItemLink href="/admin/projects/drive">Drive</PathSubItemLink>
    </PathSubItem>

    <PathSubItem segment="timeline">
      <PathSubItemLink href="/admin/projects/timeline">Timeline</PathSubItemLink>
    </PathSubItem>
  </PathItems>
</Path>

Use the optional isActive prop on Path or PathSubItem when the active state cannot be inferred from the route segment.

Account menu

DefaultSidebarAccountMenu provides the standard user trigger, settings link, theme chooser, and sign-out action. It requires the app's AuthProvider and theme provider to be present.

For a custom menu, compose the account-menu primitives in the header. The root component owns only the dropdown state and shared close behavior; the trigger reads the session user and the logout item owns its sign-out behavior.

import { SettingsIcon, UserIcon } from "lucide-react";
import {
  DropdownMenuItem,
  SidebarAccountLogoutItem,
  SidebarAccountMenu,
  SidebarAccountMenuContent,
  SidebarAccountMenuItem,
  SidebarAccountMenuLink,
  SidebarAccountMenuTrigger,
  SidebarAccountThemeMenu,
  SidebarHeader,
  DropdownMenuSeparator,
  useSidebarAccountMenu,
} from "@tulip-systems/app/components/client";

export function AdminSidebarHeader() {
  return (
    <SidebarHeader>
      <SidebarAccountMenu>
        <SidebarAccountMenuTrigger />

        <SidebarAccountMenuContent>
          <SidebarAccountMenuLink href="/admin/profile">
            <UserIcon />
            Profile
          </SidebarAccountMenuLink>

          <SidebarAccountMenuLink href="/admin/settings">
            <SettingsIcon />
            Settings
          </SidebarAccountMenuLink>

          <DropdownMenuSeparator />

          <SidebarAccountThemeMenu />

          <SidebarAccountLogoutItem />
        </SidebarAccountMenuContent>
      </SidebarAccountMenu>
    </SidebarHeader>
  );
}

To add a non-navigation action, use SidebarAccountMenuItem. It closes the dropdown and mobile sidebar after its click handler runs.

function FeedbackItem() {
  return (
    <SidebarAccountMenuItem onClick={() => openFeedbackDialog()}>
      Send feedback
    </SidebarAccountMenuItem>
  );
}

For a custom action that needs to close the menu at a different point, call useSidebarAccountMenu.

function CustomAccountItem() {
  const { close } = useSidebarAccountMenu();

  return (
    <DropdownMenuItem
      onClick={() => {
        close();
        openWorkspaceSwitcher();
      }}
    >
      Switch workspace
    </DropdownMenuItem>
  );
}

Primitives

ComponentResponsibility
SidebarSidebar shell, placement, responsive sheet behavior, and collapse variant.
SidebarHeaderTop layout slot.
SidebarContentScrollable main layout slot.
SidebarFooterBottom layout slot for persistent actions.
SidebarRailOptional desktop collapse rail.
PathGroupPermission-aware navigation section.
Path / PathLinkTop-level active route item and link.
PathItemsConditional nested-navigation list for the active parent path.
PathSubItem / PathSubItemLinkSecond-level active route item and link.
SidebarAccountMenuAccount dropdown root and shared close behavior.
DefaultSidebarAccountMenuReady-made account menu with settings, theme, and logout.

On this page