UIPackage

Sidebar 02

block sidebar
Edit on GitHub

Full-featured app sidebar -- collapsible="icon" rail with TeamSwitcher header, primary nav, projects section, secondary nav, and user dropdown footer. Each section ships as a sibling file in the same folder so consumers can edit one piece at a time.

Also available for React ->

Installation

$ npx shadcn-vue@latest add https://uipkge.dev/r/vue/sidebar-02.json

Or with the named registry: npx shadcn-vue@latest add @uipkge/sidebar-02

Examples

Used by

Files (6)

  • app/components/blocks/sidebar-02/Sidebar02.vue 2.8 kB
    <script setup lang="ts">
    import { BookOpen, Bot, Frame, LifeBuoy, Map, PieChart, Send, Settings2, SquareTerminal } from 'lucide-vue-next'
    
    import NavMain from './NavMain.vue'
    import NavProjects from './NavProjects.vue'
    import NavSecondary from './NavSecondary.vue'
    import NavUser from './NavUser.vue'
    import TeamSwitcher from './TeamSwitcher.vue'
    import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from '@/components/ui/sidebar'
    
    const data = {
      user: {
        name: 'shadcn',
        email: '[email protected]',
      },
      navMain: [
        {
          title: 'Playground',
          url: '#',
          icon: SquareTerminal,
          isActive: true,
          items: [
            {
              title: 'History',
              url: '#',
            },
            {
              title: 'Starred',
              url: '#',
            },
            {
              title: 'Settings',
              url: '#',
            },
          ],
        },
        {
          title: 'Models',
          url: '#',
          icon: Bot,
          items: [
            {
              title: 'Genesis',
              url: '#',
            },
            {
              title: 'Explorer',
              url: '#',
            },
            {
              title: 'Quantum',
              url: '#',
            },
          ],
        },
        {
          title: 'Documentation',
          url: '#',
          icon: BookOpen,
          items: [
            {
              title: 'Introduction',
              url: '#',
            },
            {
              title: 'Get Started',
              url: '#',
            },
            {
              title: 'Tutorials',
              url: '#',
            },
            {
              title: 'Changelog',
              url: '#',
            },
          ],
        },
        {
          title: 'Settings',
          url: '#',
          icon: Settings2,
          items: [
            {
              title: 'General',
              url: '#',
            },
            {
              title: 'Team',
              url: '#',
            },
            {
              title: 'Billing',
              url: '#',
            },
            {
              title: 'Limits',
              url: '#',
            },
          ],
        },
      ],
      navSecondary: [
        {
          title: 'Support',
          url: '#',
          icon: LifeBuoy,
        },
        {
          title: 'Feedback',
          url: '#',
          icon: Send,
        },
      ],
      projects: [
        {
          name: 'Design Engineering',
          url: '#',
          icon: Frame,
        },
        {
          name: 'Sales & Marketing',
          url: '#',
          icon: PieChart,
        },
        {
          name: 'Travel',
          url: '#',
          icon: Map,
        },
      ],
    }
    </script>
    
    <template>
      <Sidebar collapsible="icon">
        <SidebarHeader>
          <TeamSwitcher />
        </SidebarHeader>
        <SidebarContent>
          <NavMain :items="data.navMain" />
          <NavProjects :projects="data.projects" />
          <NavSecondary :items="data.navSecondary" class="mt-auto" />
        </SidebarContent>
        <SidebarFooter>
          <NavUser :user="data.user" />
        </SidebarFooter>
        <SidebarRail />
      </Sidebar>
    </template>
  • app/components/blocks/sidebar-02/TeamSwitcher.vue 3.9 kB
    <script setup lang="ts">
    // Edit teams + activeTeam below to match your tenant model. The dropdown
    // is the full team switcher pattern -- avatar tile, label, kbd shortcut,
    // and a "Add team" footer row. Wire setActive() to your tenant API.
    import { h, ref } from 'vue'
    import { AudioWaveform, Check, ChevronsUpDown, Command, Plus } from 'lucide-vue-next'
    
    // 4-quadrant uipkge logo. Inline functional component so it can sit
    // next to Lucide icons in the teams array without an extra file.
    const UipkgeMark = (props: { class?: string }) =>
      h('svg', { viewBox: '0 0 24 24', class: props.class, fill: 'currentColor', 'aria-hidden': 'true' }, [
        h('rect', { x: 2, y: 2, width: 9, height: 9, rx: 1.5 }),
        h('rect', { x: 13, y: 2, width: 9, height: 9, rx: 1.5, opacity: 0.55 }),
        h('rect', { x: 2, y: 13, width: 9, height: 9, rx: 1.5, opacity: 0.55 }),
        h('rect', { x: 13, y: 13, width: 9, height: 9, rx: 1.5 }),
      ])
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuLabel,
      DropdownMenuSeparator,
      DropdownMenuShortcut,
      DropdownMenuTrigger,
    } from '@/components/ui/dropdown-menu'
    import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
    
    const teams = [
      { name: 'uipkge', logo: UipkgeMark, plan: 'UI registry' },
      { name: 'uipkge HRMS', logo: AudioWaveform, plan: 'Vertical' },
      { name: 'uipkge Hospital', logo: Command, plan: 'Vertical' },
    ]
    
    const { isMobile } = useSidebar()
    const activeTeam = ref(teams[0]!)
    function setActive(team: (typeof teams)[number]) {
      activeTeam.value = team
    }
    </script>
    
    <template>
      <SidebarMenu>
        <SidebarMenuItem>
          <DropdownMenu>
            <DropdownMenuTrigger as-child>
              <SidebarMenuButton
                size="lg"
                class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
              >
                <div
                  class="bg-sidebar-primary text-sidebar-primary-foreground flex aspect-square size-8 shrink-0 items-center justify-center rounded-lg group-data-[collapsible=icon]:size-6"
                >
                  <component :is="activeTeam.logo" class="size-4 group-data-[collapsible=icon]:size-3.5" />
                </div>
                <div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                  <span class="font-display truncate font-bold">{{ activeTeam.name }}</span>
                  <span class="text-muted-foreground truncate text-xs">{{ activeTeam.plan }}</span>
                </div>
                <ChevronsUpDown class="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
              </SidebarMenuButton>
            </DropdownMenuTrigger>
            <DropdownMenuContent
              class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
              :side="isMobile ? 'bottom' : 'right'"
              align="start"
              :side-offset="4"
            >
              <DropdownMenuLabel class="text-muted-foreground text-xs">Teams</DropdownMenuLabel>
              <DropdownMenuItem v-for="(team, i) in teams" :key="team.name" class="gap-2 p-2" @select="setActive(team)">
                <div class="flex size-6 items-center justify-center rounded-sm border">
                  <component :is="team.logo" class="size-3.5 shrink-0" />
                </div>
                {{ team.name }}
                <Check v-if="activeTeam === team" class="ml-auto size-4" />
                <DropdownMenuShortcut v-else>⌘{{ i + 1 }}</DropdownMenuShortcut>
              </DropdownMenuItem>
              <DropdownMenuSeparator />
              <DropdownMenuItem class="gap-2 p-2">
                <div class="bg-background flex size-6 items-center justify-center rounded-md border">
                  <Plus class="size-4" />
                </div>
                <div class="text-muted-foreground font-medium">Add team</div>
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </SidebarMenuItem>
      </SidebarMenu>
    </template>
  • app/components/blocks/sidebar-02/NavMain.vue 2 kB
    <script setup lang="ts">
    import type { LucideIcon } from 'lucide-vue-next'
    import { ChevronRight } from 'lucide-vue-next'
    import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '@/components/ui/collapsible'
    import {
      SidebarGroup,
      SidebarGroupLabel,
      SidebarMenu,
      SidebarMenuAction,
      SidebarMenuButton,
      SidebarMenuItem,
      SidebarMenuSub,
      SidebarMenuSubButton,
      SidebarMenuSubItem,
    } from '@/components/ui/sidebar'
    
    defineProps<{
      items: {
        title: string
        url: string
        icon: LucideIcon
        isActive?: boolean
        items?: {
          title: string
          url: string
          isActive?: boolean
        }[]
      }[]
    }>()
    </script>
    
    <template>
      <SidebarGroup>
        <SidebarGroupLabel>Platform</SidebarGroupLabel>
        <SidebarMenu>
          <Collapsible v-for="item in items" :key="item.title" as-child :default-open="item.isActive">
            <SidebarMenuItem>
              <SidebarMenuButton as-child :tooltip="item.title" :is-active="item.isActive">
                <a :href="item.url">
                  <component :is="item.icon" />
                  <span>{{ item.title }}</span>
                </a>
              </SidebarMenuButton>
              <template v-if="item.items?.length">
                <CollapsibleTrigger as-child>
                  <SidebarMenuAction class="data-[state=open]:rotate-90">
                    <ChevronRight />
                    <span class="sr-only">Toggle</span>
                  </SidebarMenuAction>
                </CollapsibleTrigger>
                <CollapsibleContent>
                  <SidebarMenuSub>
                    <SidebarMenuSubItem v-for="subItem in item.items" :key="subItem.title">
                      <SidebarMenuSubButton as-child :is-active="subItem.isActive">
                        <a :href="subItem.url">
                          <span>{{ subItem.title }}</span>
                        </a>
                      </SidebarMenuSubButton>
                    </SidebarMenuSubItem>
                  </SidebarMenuSub>
                </CollapsibleContent>
              </template>
            </SidebarMenuItem>
          </Collapsible>
        </SidebarMenu>
      </SidebarGroup>
    </template>
  • app/components/blocks/sidebar-02/NavProjects.vue 2.6 kB
    <script setup lang="ts">
    import type { LucideIcon } from 'lucide-vue-next'
    import { Folder, Forward, MoreHorizontal, Trash2 } from 'lucide-vue-next'
    
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuItem,
      DropdownMenuSeparator,
      DropdownMenuTrigger,
    } from '@/components/ui/dropdown-menu'
    import {
      SidebarGroup,
      SidebarGroupLabel,
      SidebarMenu,
      SidebarMenuAction,
      SidebarMenuButton,
      SidebarMenuItem,
      useSidebar,
    } from '@/components/ui/sidebar'
    
    defineProps<{
      projects: {
        name: string
        url: string
        icon: LucideIcon
      }[]
    }>()
    
    const { isMobile } = useSidebar()
    </script>
    
    <template>
      <!-- Upstream shadcn-vue hides this entire group on icon-mode collapse
           with `group-data-[collapsible=icon]:hidden`. We keep it visible so
           the project icons stay reachable in the narrow column; the
           SidebarGroupLabel below and the `<span>` children of each
           SidebarMenuButton already self-hide on collapse, leaving an
           icon-only column that lines up with NavMain. -->
      <SidebarGroup>
        <SidebarGroupLabel>Projects</SidebarGroupLabel>
        <SidebarMenu>
          <SidebarMenuItem v-for="item in projects" :key="item.name">
            <SidebarMenuButton as-child>
              <a :href="item.url">
                <component :is="item.icon" />
                <span>{{ item.name }}</span>
              </a>
            </SidebarMenuButton>
            <DropdownMenu>
              <DropdownMenuTrigger as-child>
                <SidebarMenuAction show-on-hover>
                  <MoreHorizontal />
                  <span class="sr-only">More</span>
                </SidebarMenuAction>
              </DropdownMenuTrigger>
              <DropdownMenuContent
                class="w-48 rounded-lg"
                :side="isMobile ? 'bottom' : 'right'"
                :align="isMobile ? 'end' : 'start'"
              >
                <DropdownMenuItem>
                  <Folder class="text-muted-foreground" />
                  <span>View Project</span>
                </DropdownMenuItem>
                <DropdownMenuItem>
                  <Forward class="text-muted-foreground" />
                  <span>Share Project</span>
                </DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem>
                  <Trash2 class="text-muted-foreground" />
                  <span>Delete Project</span>
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </SidebarMenuItem>
          <SidebarMenuItem>
            <SidebarMenuButton>
              <MoreHorizontal />
              <span>More</span>
            </SidebarMenuButton>
          </SidebarMenuItem>
        </SidebarMenu>
      </SidebarGroup>
    </template>
  • app/components/blocks/sidebar-02/NavSecondary.vue 0.8 kB
    <script setup lang="ts">
    import type { LucideIcon } from 'lucide-vue-next'
    
    import {
      SidebarGroup,
      SidebarGroupContent,
      SidebarMenu,
      SidebarMenuButton,
      SidebarMenuItem,
    } from '@/components/ui/sidebar'
    
    const props = defineProps<{
      items: {
        title: string
        url: string
        icon: LucideIcon
      }[]
    }>()
    </script>
    
    <template>
      <SidebarGroup>
        <SidebarGroupContent>
          <SidebarMenu>
            <SidebarMenuItem v-for="item in items" :key="item.title">
              <SidebarMenuButton as-child size="sm" :is-active="item.isActive">
                <a :href="item.url">
                  <component :is="item.icon" />
                  <span>{{ item.title }}</span>
                </a>
              </SidebarMenuButton>
            </SidebarMenuItem>
          </SidebarMenu>
        </SidebarGroupContent>
      </SidebarGroup>
    </template>
  • app/components/blocks/sidebar-02/NavUser.vue 4.6 kB
    <script setup lang="ts">
    import { computed } from 'vue'
    import {
      BadgeCheck,
      Bell,
      ChevronsUpDown,
      CreditCard,
      LogOut,
      Monitor,
      Moon,
      Palette,
      Sparkles,
      Sun,
    } from 'lucide-vue-next'
    import { useTheme } from '~/composables/useTheme'
    
    import { Avatar, AvatarFallback } from '@/components/ui/avatar'
    import {
      DropdownMenu,
      DropdownMenuContent,
      DropdownMenuGroup,
      DropdownMenuItem,
      DropdownMenuLabel,
      DropdownMenuRadioGroup,
      DropdownMenuRadioItem,
      DropdownMenuSeparator,
      DropdownMenuSub,
      DropdownMenuSubContent,
      DropdownMenuSubTrigger,
      DropdownMenuTrigger,
    } from '@/components/ui/dropdown-menu'
    import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '@/components/ui/sidebar'
    
    const props = defineProps<{
      user: {
        name: string
        email: string
      }
    }>()
    
    const { isMobile } = useSidebar()
    const { theme, setTheme } = useTheme()
    
    const initials = computed(() => {
      const parts = props.user.name.trim().split(/\s+/).slice(0, 2)
      return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U'
    })
    </script>
    
    <template>
      <SidebarMenu>
        <SidebarMenuItem>
          <DropdownMenu>
            <DropdownMenuTrigger as-child>
              <SidebarMenuButton
                size="lg"
                class="data-[state=open]:bg-sidebar-accent data-[state=open]:text-sidebar-accent-foreground group-data-[collapsible=icon]:!justify-center"
              >
                <Avatar class="h-8 w-8 shrink-0 rounded-lg group-data-[collapsible=icon]:size-6">
                  <AvatarFallback class="rounded-lg text-xs group-data-[collapsible=icon]:text-[10px]">{{
                    initials
                  }}</AvatarFallback>
                </Avatar>
                <div class="grid flex-1 text-left text-sm leading-tight group-data-[collapsible=icon]:hidden">
                  <span class="truncate font-medium">{{ user.name }}</span>
                  <span class="truncate text-xs">{{ user.email }}</span>
                </div>
                <ChevronsUpDown class="ml-auto size-4 group-data-[collapsible=icon]:hidden" />
              </SidebarMenuButton>
            </DropdownMenuTrigger>
            <DropdownMenuContent
              class="w-(--reka-dropdown-menu-trigger-width) min-w-56 rounded-lg"
              :side="isMobile ? 'bottom' : 'right'"
              align="end"
              :side-offset="4"
            >
              <DropdownMenuLabel class="p-0 font-normal">
                <div class="flex items-center gap-2 px-1 py-1.5 text-left text-sm">
                  <Avatar class="h-8 w-8 rounded-lg">
                    <AvatarFallback class="rounded-lg text-xs">{{ initials }}</AvatarFallback>
                  </Avatar>
                  <div class="grid flex-1 text-left text-sm leading-tight">
                    <span class="truncate font-semibold">{{ user.name }}</span>
                    <span class="truncate text-xs">{{ user.email }}</span>
                  </div>
                </div>
              </DropdownMenuLabel>
              <DropdownMenuSeparator />
              <DropdownMenuGroup>
                <DropdownMenuItem>
                  <Sparkles />
                  Upgrade to Pro
                </DropdownMenuItem>
              </DropdownMenuGroup>
              <DropdownMenuSeparator />
              <DropdownMenuGroup>
                <DropdownMenuItem>
                  <BadgeCheck />
                  Account
                </DropdownMenuItem>
                <DropdownMenuItem>
                  <CreditCard />
                  Billing
                </DropdownMenuItem>
                <DropdownMenuItem>
                  <Bell />
                  Notifications
                </DropdownMenuItem>
              </DropdownMenuGroup>
              <DropdownMenuSeparator />
              <DropdownMenuSub>
                <DropdownMenuSubTrigger>
                  <Palette />
                  Theme
                  <span class="text-muted-foreground ml-auto text-xs capitalize">{{ theme }}</span>
                </DropdownMenuSubTrigger>
                <DropdownMenuSubContent class="min-w-36">
                  <DropdownMenuRadioGroup
                    :model-value="theme"
                    @update:model-value="(v) => setTheme(v as 'light' | 'dark' | 'system')"
                  >
                    <DropdownMenuRadioItem value="light"> <Sun /> Light </DropdownMenuRadioItem>
                    <DropdownMenuRadioItem value="dark"> <Moon /> Dark </DropdownMenuRadioItem>
                    <DropdownMenuRadioItem value="system"> <Monitor /> System </DropdownMenuRadioItem>
                  </DropdownMenuRadioGroup>
                </DropdownMenuSubContent>
              </DropdownMenuSub>
              <DropdownMenuSeparator />
              <DropdownMenuItem>
                <LogOut />
                Log out
              </DropdownMenuItem>
            </DropdownMenuContent>
          </DropdownMenu>
        </SidebarMenuItem>
      </SidebarMenu>
    </template>

Raw manifest: https://uipkge.dev/r/vue/sidebar-02.json