Team Switcher Sidebar
blocklayoutFull-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.
Installation
$pnpm dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar-02.json$npx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar-02.json$yarn dlx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar-02.json$bunx shadcn-svelte@latest add https://uipkge.dev/r/svelte/sidebar-02.jsonManifest:
https://uipkge.dev/r/svelte/sidebar-02.jsonInstalls to:src/lib/components/blocks/sidebar-02/Variants
Loading interactive previews…
Loading interactive previews…
npm dependencies
Used by
Files installed (8)
src/lib/components/blocks/sidebar-02/Sidebar02.svelte3.1 kB
<script lang="ts"> import type { Component } from 'svelte' import { BookOpen, Bot, Frame, LifeBuoy, Map, PieChart, Send, Settings2, SquareTerminal } from '@lucide/svelte' import NavMain from './NavMain.svelte' import NavProjects from './NavProjects.svelte' import NavSecondary from './NavSecondary.svelte' import NavUser from './NavUser.svelte' import TeamSwitcher from './TeamSwitcher.svelte' import { Sidebar, SidebarContent, SidebarFooter, SidebarHeader, SidebarRail } from '$lib/components/ui/sidebar' const data = { user: { name: 'shadcn', email: '[email protected]', }, navMain: [ { title: 'Playground', url: '#', icon: SquareTerminal as Component, isActive: true, items: [ { title: 'History', url: '#', }, { title: 'Starred', url: '#', }, { title: 'Settings', url: '#', }, ], }, { title: 'Models', url: '#', icon: Bot as Component, items: [ { title: 'Genesis', url: '#', }, { title: 'Explorer', url: '#', }, { title: 'Quantum', url: '#', }, ], }, { title: 'Documentation', url: '#', icon: BookOpen as Component, items: [ { title: 'Introduction', url: '#', }, { title: 'Get Started', url: '#', }, { title: 'Tutorials', url: '#', }, { title: 'Changelog', url: '#', }, ], }, { title: 'Settings', url: '#', icon: Settings2 as Component, items: [ { title: 'General', url: '#', }, { title: 'Team', url: '#', }, { title: 'Billing', url: '#', }, { title: 'Limits', url: '#', }, ], }, ], navSecondary: [ { title: 'Support', url: '#', icon: LifeBuoy as Component, }, { title: 'Feedback', url: '#', icon: Send as Component, }, ], projects: [ { name: 'Design Engineering', url: '#', icon: Frame as Component, }, { name: 'Sales & Marketing', url: '#', icon: PieChart as Component, }, { name: 'Travel', url: '#', icon: Map as Component, }, ], } </script> <Sidebar data-slot="sidebar-02" 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>src/lib/components/blocks/sidebar-02/TeamSwitcher.svelte3.6 kB
<script 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 type { Component } from 'svelte' import { AudioWaveform, Check, ChevronsUpDown, Command, Plus } from '@lucide/svelte' import UipkgeMark from './UipkgeMark.svelte' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuTrigger, } from '$lib/components/ui/dropdown-menu' import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '$lib/components/ui/sidebar' const teams: { name: string; logo: Component; plan: string }[] = [ { name: 'uipkge', logo: UipkgeMark, plan: 'UI registry' }, { name: 'uipkge HRMS', logo: AudioWaveform, plan: 'Vertical' }, { name: 'uipkge Hospital', logo: Command, plan: 'Vertical' }, ] // Read via `sidebar.isMobile` (not destructured) to stay subscribed. const sidebar = useSidebar() let activeTeam = $state(teams[0]!) function setActive(team: (typeof teams)[number]) { activeTeam = team } const ActiveLogo = $derived(activeTeam.logo) </script> <SidebarMenu data-slot="sidebar-02"> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> {#snippet child({ props })} <SidebarMenuButton {...props} 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" > <ActiveLogo 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> {/snippet} </DropdownMenuTrigger> <DropdownMenuContent class="min-w-56 rounded-lg" side={sidebar.isMobile ? 'bottom' : 'right'} align="start" sideOffset={4} > <DropdownMenuLabel class="text-muted-foreground text-xs">Teams</DropdownMenuLabel> {#each teams as team, i (team.name)} {@const TeamLogo = team.logo} <DropdownMenuItem class="gap-2 p-2" onSelect={() => setActive(team)}> <div class="flex size-6 items-center justify-center rounded-sm border"> <TeamLogo class="size-3.5 shrink-0" /> </div> {team.name} {#if activeTeam === team} <Check class="ml-auto size-4" /> {:else} <DropdownMenuShortcut>⌘{i + 1}</DropdownMenuShortcut> {/if} </DropdownMenuItem> {/each} <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>src/lib/components/blocks/sidebar-02/NavMain.svelte2.6 kB
<script lang="ts" module> import type { Component } from 'svelte' export interface NavMainItem { title: string url: string icon: Component isActive?: boolean items?: { title: string url: string isActive?: boolean }[] } export interface NavMainProps { items: NavMainItem[] } </script> <script lang="ts"> import { ChevronRight } from '@lucide/svelte' import { Collapsible, CollapsibleContent, CollapsibleTrigger } from '$lib/components/ui/collapsible' import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, SidebarMenuSub, SidebarMenuSubButton, SidebarMenuSubItem, } from '$lib/components/ui/sidebar' let { items }: NavMainProps = $props() </script> <SidebarGroup data-slot="sidebar-02"> <SidebarGroupLabel>Platform</SidebarGroupLabel> <SidebarMenu> {#each items as item (item.title)} {@const ItemIcon = item.icon} <!-- The Svelte Collapsible has no as-child mode, so it renders its own wrapper inside the menu item where the Vue twin merges the two. --> <SidebarMenuItem> <Collapsible defaultOpen={item.isActive}> <SidebarMenuButton tooltip={item.title} isActive={item.isActive}> {#snippet child({ props })} <a href={item.url} {...props}> <ItemIcon /> <span>{item.title}</span> </a> {/snippet} </SidebarMenuButton> {#if item.items?.length} <CollapsibleTrigger> {#snippet child({ props })} <SidebarMenuAction {...props} class="data-[state=open]:rotate-90"> <ChevronRight /> <span class="sr-only">Toggle</span> </SidebarMenuAction> {/snippet} </CollapsibleTrigger> <CollapsibleContent> <SidebarMenuSub> {#each item.items as subItem (subItem.title)} <SidebarMenuSubItem> <SidebarMenuSubButton isActive={subItem.isActive}> {#snippet child({ props })} <a href={subItem.url} {...props}> <span>{subItem.title}</span> </a> {/snippet} </SidebarMenuSubButton> </SidebarMenuSubItem> {/each} </SidebarMenuSub> </CollapsibleContent> {/if} </Collapsible> </SidebarMenuItem> {/each} </SidebarMenu> </SidebarGroup>src/lib/components/blocks/sidebar-02/NavProjects.svelte2.9 kB
<script lang="ts" module> import type { Component } from 'svelte' export interface NavProjectsProps { projects: { name: string url: string icon: Component }[] } </script> <script lang="ts"> import { Folder, Forward, MoreHorizontal, Trash2 } from '@lucide/svelte' import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '$lib/components/ui/dropdown-menu' import { SidebarGroup, SidebarGroupLabel, SidebarMenu, SidebarMenuAction, SidebarMenuButton, SidebarMenuItem, useSidebar, } from '$lib/components/ui/sidebar' let { projects }: NavProjectsProps = $props() // Read via `sidebar.isMobile` (not destructured) to stay subscribed. const sidebar = useSidebar() </script> <!-- 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> {#each projects as item (item.name)} {@const ItemIcon = item.icon} <SidebarMenuItem> <SidebarMenuButton> {#snippet child({ props })} <a href={item.url} {...props}> <ItemIcon /> <span>{item.name}</span> </a> {/snippet} </SidebarMenuButton> <DropdownMenu> <DropdownMenuTrigger> {#snippet child({ props })} <SidebarMenuAction {...props} showOnHover> <MoreHorizontal /> <span class="sr-only">More</span> </SidebarMenuAction> {/snippet} </DropdownMenuTrigger> <DropdownMenuContent class="w-48 rounded-lg" side={sidebar.isMobile ? 'bottom' : 'right'} align={sidebar.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> {/each} <SidebarMenuItem> <SidebarMenuButton> <MoreHorizontal /> <span>More</span> </SidebarMenuButton> </SidebarMenuItem> </SidebarMenu> </SidebarGroup>src/lib/components/blocks/sidebar-02/NavSecondary.svelte1.1 kB
<script lang="ts" module> import type { Component } from 'svelte' export interface NavSecondaryProps { items: { title: string url: string icon: Component isActive?: boolean }[] class?: string } </script> <script lang="ts"> import { SidebarGroup, SidebarGroupContent, SidebarMenu, SidebarMenuButton, SidebarMenuItem, } from '$lib/components/ui/sidebar' let { items, class: className }: NavSecondaryProps = $props() </script> <SidebarGroup data-slot="sidebar-02" class={className}> <SidebarGroupContent> <SidebarMenu> {#each items as item (item.title)} {@const ItemIcon = item.icon} <SidebarMenuItem> <SidebarMenuButton size="sm" isActive={item.isActive}> {#snippet child({ props })} <a href={item.url} {...props}> <ItemIcon /> <span>{item.title}</span> </a> {/snippet} </SidebarMenuButton> </SidebarMenuItem> {/each} </SidebarMenu> </SidebarGroupContent> </SidebarGroup>src/lib/components/blocks/sidebar-02/NavUser.svelte4.9 kB
<script lang="ts" module> export interface NavUserProps { user: { name: string email: string } } </script> <script lang="ts"> import { BadgeCheck, Bell, ChevronsUpDown, CreditCard, LogOut, Monitor, Moon, Palette, Sun, Zap, } from '@lucide/svelte' import { Avatar, AvatarFallback } from '$lib/components/ui/avatar' import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from '$lib/components/ui/dropdown-menu' import { SidebarMenu, SidebarMenuButton, SidebarMenuItem, useSidebar } from '$lib/components/ui/sidebar' let { user }: NavUserProps = $props() // Read via `sidebar.isMobile` (not destructured) to stay subscribed. const sidebar = useSidebar() // The Svelte registry ships no use-theme hook (yet), so the block owns a // tiny local theme state and applies the dark class itself. Swap this for // your own theme store after install. type Theme = 'light' | 'dark' | 'system' let theme = $state<Theme>('system') $effect(() => { const root = document.documentElement const dark = theme === 'dark' || (theme === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches) root.classList.toggle('dark', dark) }) const initials = $derived.by(() => { const parts = user.name.trim().split(/\s+/).slice(0, 2) return parts.map((p) => p[0]?.toUpperCase()).join('') || 'U' }) </script> <SidebarMenu data-slot="sidebar-02"> <SidebarMenuItem> <DropdownMenu> <DropdownMenuTrigger> {#snippet child({ props })} <SidebarMenuButton {...props} 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">{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> {/snippet} </DropdownMenuTrigger> <DropdownMenuContent class="min-w-56 rounded-lg" side={sidebar.isMobile ? 'bottom' : 'right'} align="end" sideOffset={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> <Zap /> 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 value={theme} onValueChange={(v) => (theme = 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>src/lib/components/blocks/sidebar-02/UipkgeMark.svelte0.6 kB
<!-- 4-quadrant uipkge logo. Sits next to Lucide icons in the teams array. --> <script lang="ts" module> export interface UipkgeMarkProps { class?: string } </script> <script lang="ts"> let { class: className }: UipkgeMarkProps = $props() </script> <svg viewBox="0 0 24 24" class={className} fill="currentColor" aria-hidden="true"> <rect x="2" y="2" width="9" height="9" rx="1.5" /> <rect x="13" y="2" width="9" height="9" rx="1.5" opacity="0.55" /> <rect x="2" y="13" width="9" height="9" rx="1.5" opacity="0.55" /> <rect x="13" y="13" width="9" height="9" rx="1.5" /> </svg>src/routes/sidebar-02-demo/+page.svelte1 kB
<script lang="ts"> // Demo page installed by the sidebar-02 item. Visit /sidebar-02-demo after // install to see the block without editing your own routes. Free to edit or // delete me. import Sidebar02 from '$lib/components/blocks/sidebar-02/Sidebar02.svelte' import { SidebarInset, SidebarProvider, SidebarTrigger } from '$lib/components/ui/sidebar' </script> <SidebarProvider> <Sidebar02 /> <SidebarInset> <header class="border-border flex h-14 shrink-0 items-center gap-3 border-b px-4"> <SidebarTrigger class="size-7" /> <h1 class="text-sm font-semibold">sidebar-02 demo</h1> </header> <div class="flex-1 p-6"> <div class="grid auto-rows-min gap-4 md:grid-cols-3"> <div class="bg-muted/50 aspect-video rounded-xl"></div> <div class="bg-muted/50 aspect-video rounded-xl"></div> <div class="bg-muted/50 aspect-video rounded-xl"></div> </div> <div class="bg-muted/50 mt-4 min-h-[60vh] rounded-xl"></div> </div> </SidebarInset> </SidebarProvider>
Raw manifest:https://uipkge.dev/r/svelte/sidebar-02.json