# Sidebar Double-Sided

> A sidebar double-sided block.

A shadcn/ui block from the [@blocks-so](https://blocks.so) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/blocks-so/sidebar-05).

- Page, with a live preview: https://shadcn.exchange/r/blocks-so/sidebar-05
- Screenshot: https://abzjhwgwpdpz7ua5.public.blob.vercel-storage.com/shots/blocks-so/sidebar-05-light-1f73fa203fa0b5b0.png
- Registry item: https://blocks.so/r/sidebar-05.json
- Categories: sidebar
- Listed: 2026-10-13, source last changed 2026-10-13

## Install

```bash
npx shadcn@latest add https://blocks.so/r/sidebar-05.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `@tabler/icons-react`, `lucide-react`
- Registry items: `avatar`, `dropdown-menu`, `sidebar`

## Files

### `content/components/sidebar/sidebar-05/team-switcher.tsx`

```tsx
'use client';

import { ChevronsUpDown, Plus } from 'lucide-react';
import * as React from 'react';

import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuLabel,
  DropdownMenuSeparator,
  DropdownMenuShortcut,
  DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
  SidebarMenu,
  SidebarMenuButton,
  SidebarMenuItem,
  useSidebar,
} from '@/components/ui/sidebar';

export function TeamSwitcher({
  teams,
}: {
  teams: {
    name: string;
    logo: React.ElementType;
    plan: string;
  }[];
}) {
  const { isMobile } = useSidebar();
  const [activeTeam, setActiveTeam] = React.useState(teams[0]);

  if (!activeTeam) {
    return null;
  }

  return (
    <SidebarMenu>
      <SidebarMenuItem>
        <DropdownMenu>
          <DropdownMenuTrigger
            render={
              <SidebarMenuButton
                className="data-popup-open:bg-sidebar-accent data-popup-open:text-sidebar-accent-foreground"
                size="lg"
              />
            }
          >
            <div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-accent text-sidebar-accent-foreground">
              <activeTeam.logo className="size-4" />
            </div>
            <div className="grid flex-1 text-left text-sm leading-tight">
              <span className="truncate font-medium">{activeTeam.name}</span>
              <span className="truncate text-xs">{activeTeam.plan}</span>
            </div>
            <ChevronsUpDown className="ml-auto" />
          </DropdownMenuTrigger>
          <DropdownMenuContent
            align="start"
            className="w-(--anchor-width) min-w-56 rounded-lg"
            side={isMobile ? 'bottom' : 'right'}
            sideOffset={4}
          >
            <DropdownMenuLabel className="text-muted-foreground text-xs">
              Teams
            </DropdownMenuLabel>
            {teams.map((team, index) => (
              <DropdownMenuItem
                className="gap-2 p-2"
                key={team.name}
                onClick={() => setActiveTeam(team)}
              >
                <div className="flex size-6 items-center justify-center rounded-md border">
                  <team.logo className="size-3.5 shrink-0" />
                </div>
                {team.name}
                <DropdownMenuShortcut>⌘{index + 1}</DropdownMenuShortcut>
              </DropdownMenuItem>
            ))}
            <DropdownMenuSeparator />
            <DropdownMenuItem className="gap-2 p-2">
              <div className="flex size-6 items-center justify-center rounded-md border bg-transparent">
                <Plus className="size-4" />
              </div>
              <div className="font-medium text-muted-foreground">Add team</div>
            </DropdownMenuItem>
          </DropdownMenuContent>
        </DropdownMenu>
      </SidebarMenuItem>
    </SidebarMenu>
  );
}
```

### `content/components/sidebar/sidebar-05/app/page.tsx`

```tsx
'use client';

import { SidebarProvider } from '@/components/ui/sidebar';
import { AppSidebar } from '@/components/sidebar-05/app-sidebar';

export default function Page() {
  return (
    <SidebarProvider>
      <div className="flex h-dvh w-full">
        <AppSidebar />
      </div>
    </SidebarProvider>
  );
}
```

### `content/components/sidebar/sidebar-05/app-sidebar.tsx`

```tsx
'use client';

import {
  IconActivityHeartbeat,
  IconArchive,
  IconBackground,
  IconBellRinging,
  IconBrandGoogle,
  IconBrandMeta,
  IconBrandNpm,
  IconBrandOpenai,
  IconBug,
  IconChartBar,
  IconChevronRight,
  IconCloud,
  IconDatabase,
  IconFileText,
  IconFolder,
  IconFolders,
  IconGitCommit,
  IconGitMerge,
  IconGitPullRequest,
  IconHome,
  IconKey,
  IconLockExclamation,
  IconLockPassword,
  IconLogout,
  IconNorthStar,
  IconPackageExport,
  IconPackages,
  IconPasswordFingerprint,
  IconPlayerPlay,
  IconScanEye,
  IconSettings,
  IconShieldLock,
  IconStar,
  IconTarget,
  IconTerminal2,
  IconUser,
  IconUserPlus,
  IconWebhook,
  IconX,
} from '@tabler/icons-react';
import type React from 'react';
import { useState } from 'react';
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
import {
  Sidebar,
  SidebarContent,
  SidebarFooter,
  SidebarGroup,
  SidebarGroupContent,
  SidebarHeader,
  SidebarMenu,
  SidebarMenuBadge,
  SidebarMenuButton,
  SidebarMenuItem,
} from '@/components/ui/sidebar';
import { cn } from '@/lib/utils';
import { TeamSwitcher } from '@/components/sidebar-05/team-switcher';

const data = {
  teams: [
    {
      name: 'OpenAI',
      logo: IconBrandOpenai,
      plan: 'Enterprise',
    },
    {
      name: 'Anthropic',
      logo: IconNorthStar,
      plan: 'Pro',
    },
    {
      name: 'Google',
      logo: IconBrandGoogle,
      plan: 'Free',
    },
    {
      name: 'Meta',
      logo: IconBrandMeta,
      plan: 'Free',
    },
  ],
};

interface SidebarItem {
  id: string;
  label: string;
  icon: React.ComponentType<{ className?: string }>;
  badge?: string;
  route?: string;
  subItems?: {
    id: string;
    label: string;
    icon: React.ComponentType<{ className?: string }>;
    description?: string;
    route?: string;
  }[];
}

const sidebarItems: SidebarItem[] = [
  {
    id: 'overview',
    label: 'Overview',
    icon: IconHome,
    subItems: [
      {
        id: 'dashboard',
        label: 'Dashboard',
        icon: IconChartBar,
        description: 'Project overview and activity',
        route: '/overview/dashboard',
      },
      {
        id: 'activity',
        label: 'Activity',
        icon: IconActivityHeartbeat,
        description: 'Recent commits and changes',
        route: '/overview/activity',
      },
      {
        id: 'insights',
        label: 'Insights',
        icon: IconTarget,
        description: 'Code analytics and metrics',
        route: '/overview/insights',
      },
    ],
  },
  {
    id: 'repositories',
    label: 'Repositories',
    icon: IconFolders,
    badge: '12',
    subItems: [
      {
        id: 'all-repos',
        label: 'All Repositories',
        icon: IconFolder,
        description: 'Browse all your repositories',
        route: '/repositories',
      },
      {
        id: 'starred',
        label: 'Starred',
        icon: IconStar,
        description: 'Your starred repositories',
        route: '/repositories/starred',
      },
      {
        id: 'archived',
        label: 'Archived',
        icon: IconArchive,
        description: 'Archived repositories',
        route: '/repositories/archived',
      },
    ],
  },
  {
    id: 'pull-requests',
    label: 'Pull Requests',
    icon: IconGitPullRequest,
    badge: '3',
    subItems: [
      {
        id: 'open-prs',
        label: 'Open',
        icon: IconGitPullRequest,
        description: 'Open pull requests',
        route: '/pull-requests/open',
      },
      {
        id: 'review-requests',
        label: 'Review Requests',
        icon: IconScanEye,
        description: 'PRs awaiting your review',
        route: '/pull-requests/review',
      },
      {
        id: 'merged',
        label: 'Merged',
        icon: IconGitMerge,
        description: 'Recently merged PRs',
        route: '/pull-requests/merged',
      },
    ],
  },
  {
    id: 'issues',
    label: 'Issues',
    icon: IconBug,
    badge: '7',
    subItems: [
      {
        id: 'open-issues',
        label: 'Open Issues',
        icon: IconBug,
        description: 'Active issues and bugs',
        route: '/issues/open',
      },
      {
        id: 'assigned',
        label: 'Assigned to Me',
        icon: IconUserPlus,
        description: 'Issues assigned to you',
        route: '/issues/assigned',
      },
      {
        id: 'created',
        label: 'Created by Me',
        icon: IconGitCommit,
        description: "Issues you've created",
        route: '/issues/created',
      },
    ],
  },
  {
    id: 'actions',
    label: 'Actions',
    icon: IconBackground,
    subItems: [
      {
        id: 'workflows',
        label: 'Workflows',
        icon: IconPlayerPlay,
        description: 'CI/CD workflows and pipelines',
        route: '/actions/workflows',
      },
      {
        id: 'runners',
        label: 'Runners',
        icon: IconTerminal2,
        description: 'Self-hosted runners',
        route: '/actions/runners',
      },
      {
        id: 'deployments',
        label: 'Deployments',
        icon: IconCloud,
        description: 'Deployment history',
        route: '/actions/deployments',
      },
    ],
  },
  {
    id: 'packages',
    label: 'Packages',
    icon: IconPackages,
    subItems: [
      {
        id: 'published',
        label: 'Published',
        icon: IconPackageExport,
        description: 'Your published packages',
        route: '/packages/published',
      },
      {
        id: 'container-registry',
        label: 'Container Registry',
        icon: IconDatabase,
        description: 'Docker images and containers',
        route: '/packages/containers',
      },
      {
        id: 'npm-packages',
        label: 'npm Packages',
        icon: IconBrandNpm,
        description: 'Node.js packages',
        route: '/packages/npm',
      },
    ],
  },
  {
    id: 'security',
    label: 'Security',
    icon: IconLockPassword,
    badge: '2',
    subItems: [
      {
        id: 'alerts',
        label: 'Security Alerts',
        icon: IconLockExclamation,
        description: 'Vulnerability alerts',
        route: '/security/alerts',
      },
      {
        id: 'advisories',
        label: 'Advisories',
        icon: IconShieldLock,
        description: 'Security advisories',
        route: '/security/advisories',
      },
      {
        id: 'secrets',
        label: 'Secrets',
        icon: IconPasswordFingerprint,
        description: 'Repository secrets',
        route: '/security/secrets',
      },
    ],
  },
  {
    id: 'settings',
    label: 'Settings',
    icon: IconSettings,
    subItems: [
      {
        id: 'profile',
        label: 'Profile',
        icon: IconUser,
        description: 'Your profile settings',
        route: '/settings/profile',
      },
      {
        id: 'notifications',
        label: 'Notifications',
        icon: IconBellRinging,
        description: 'Notification preferences',
        route: '/settings/notifications',
      },
      {
        id: 'webhooks',
        label: 'Webhooks',
        icon: IconWebhook,
        description: 'Webhook configurations',
        route: '/settings/webhooks',
      },
      {
        id: 'api-keys',
        label: 'API Keys',
        icon: IconKey,
        description: 'Personal access tokens',
        route: '/settings/api-keys',
      },
    ],
  },
  {
    id: 'docs',
    label: 'Documentation',
    icon: IconFileText,
    route: '/docs',
  },
];

export function AppSidebar() {
  const [activeItem, setActiveItem] = useState<string | null>('overview');
  const [selectedSubItem, setSelectedSubItem] = useState<string | null>(null);

  const activeItemData = sidebarItems.find((item) => item.id === activeItem);

  const handleItemClick = (item: SidebarItem) => {
    if (item.subItems) {
      const isActive = activeItem === item.id;
      setActiveItem(isActive ? null : item.id);
      if (isActive) {
        setSelectedSubItem(null);
      }
    } else {
      if (activeItem) {
        setActiveItem(null);
        setSelectedSubItem(null);
      }
      console.log(`[v0] Navigating to: ${item.route}`);
    }
  };

  const handleSubItemClick = (subItem: { id: string; route?: string }) => {
    setSelectedSubItem(selectedSubItem === subItem.id ? null : subItem.id);
    if (subItem.route) {
      console.log(`[v0] Navigating to: ${subItem.route}`);
    }
  };

  return (
    <div className="flex h-dvh bg-background">
      <Sidebar
        className="w-64 border-r"
        collapsible="none"
        side="left"
        variant="sidebar"
      >
        <SidebarHeader>
          <TeamSwitcher teams={data.teams} />
        </SidebarHeader>

        <SidebarContent>
          <SidebarGroup>
            <SidebarGroupContent>
              <SidebarMenu>
                {sidebarItems.map((item) => {
                  const Icon = item.icon;
                  const isActive = activeItem === item.id;
                  const chevronIndicator = (
                    <IconChevronRight
                      className={cn(
                        'h-4 w-4 shrink-0 transition-transform',
                        isActive && 'rotate-90'
                      )}
                    />
                  );

                  return (
                    <SidebarMenuItem key={item.id}>
                      <SidebarMenuButton
                        className="h-10 w-full px-3"
                        isActive={isActive}
                        onClick={() => handleItemClick(item)}
                      >
                        <div className="flex min-w-0 flex-1 items-center gap-3">
                          <Icon className="h-4 w-4 shrink-0" />
                          <span className="truncate">{item.label}</span>
                        </div>
                        <div className="ml-auto flex min-w-fit shrink-0 items-center gap-1">
                          {(item.badge || item.subItems) &&
                            (item.badge ? (
                              <SidebarMenuBadge
                                className={cn(
                                  'min-w-fit',
                                  item.subItems && 'gap-x-3'
                                )}
                              >
                                {item.badge}
                                {item.subItems && chevronIndicator}
                              </SidebarMenuBadge>
                            ) : (
                              chevronIndicator
                            ))}
                        </div>
                      </SidebarMenuButton>
                    </SidebarMenuItem>
                  );
                })}
              </SidebarMenu>
            </SidebarGroupContent>
          </SidebarGroup>
        </SidebarContent>

        <SidebarFooter>
          <SidebarMenu>
            <SidebarMenuItem>
              <SidebarMenuButton className="h-12 w-full px-3">
                <div className="flex min-w-0 flex-1 items-center gap-3">
                  <Avatar className="h-8 w-8 rounded-full">
                    <AvatarImage alt="ephraim" src="/avatar-01.png" />
                    <AvatarFallback className="rounded-full">E</AvatarFallback>
                  </Avatar>
                  <div className="min-w-0 flex-1 text-left">
                    <div className="truncate font-medium text-sm">ephraim</div>
                    <div className="truncate text-muted-foreground text-xs">
                      ephraim@blocks.so
                    </div>
                  </div>
                </div>
                <IconLogout className="h-4 w-4 shrink-0" />
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarFooter>
      </Sidebar>

      {activeItem && activeItemData?.subItems && (
        <Sidebar
          className="slide-in-from-left-5 w-72 animate-in border-r duration-200"
          collapsible="none"
          side="left"
          variant="sidebar"
        >
          <SidebarHeader className="flex flex-row items-center justify-between border-b px-4">
            <h3 className="font-medium">{activeItemData.label}</h3>
            <button
              className="flex h-6 w-6 items-center justify-center rounded-md p-0 hover:bg-sidebar-accent"
              onClick={() => setActiveItem(null)}
            >
              <IconX className="h-4 w-4" />
            </button>
          </SidebarHeader>

          <SidebarContent>
            <SidebarGroup>
              <SidebarGroupContent>
                <SidebarMenu>
                  {activeItemData.subItems.map((subItem) => {
                    const SubIcon = subItem.icon;
                    const isSelected = selectedSubItem === subItem.id;

                    return (
                      <SidebarMenuItem key={subItem.id}>
                        <SidebarMenuButton
                          className="h-auto w-full justify-start gap-3 px-3 py-2"
                          isActive={isSelected}
                          onClick={() => handleSubItemClick(subItem)}
                        >
                          <SubIcon className="mt-0.5 h-5 w-5 shrink-0 self-start" />

                          <div className="min-w-0 flex-1 text-left">
                            <div className="font-medium">{subItem.label}</div>
                            {subItem.description && (
                              <div className="mt-0.5 text-muted-foreground text-xs">
                                {subItem.description}
                              </div>
                            )}
                          </div>
                        </SidebarMenuButton>
                      </SidebarMenuItem>
                    );
                  })}
                </SidebarMenu>
              </SidebarGroupContent>
            </SidebarGroup>
          </SidebarContent>
        </Sidebar>
      )}
    </div>
  );
}
```
