A collapsible sidebar navigation inspired by Linear. Includes grouped nav items, active states, count badges, and smooth collapse animation.
WORKSPACE
MY TEAMS
FAVORITES
Select an item from the sidebar
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import {
Inbox, FileText, Eye, FolderOpen, AlertCircle, RotateCcw,
Map, Archive, Star, Bug, Layers, Settings, UserPlus,
ChevronRight, ChevronLeft, ChevronDown,
} from 'lucide-react';
import type { LucideIcon } from 'lucide-react';
function cn(...classes: (string | false | null | undefined)[]) {
return classes.filter(Boolean).join(' ');
}
type NavItem = {
id: string;
label: string;
icon: LucideIcon;
count?: number;
section: string;
};
const NAV_ITEMS: NavItem[] = [
{ id: 'inbox', label: 'Inbox', icon: Inbox, count: 3, section: 'WORKSPACE' },
{ id: 'my-issues', label: 'My Issues', icon: FileText, section: 'WORKSPACE' },
{ id: 'views', label: 'Views', icon: Eye, section: 'WORKSPACE' },
{ id: 'projects', label: 'Projects', icon: FolderOpen, section: 'WORKSPACE' },
{ id: 'issues', label: 'Issues', icon: AlertCircle, section: 'MY TEAMS' },
{ id: 'cycles', label: 'Cycles', icon: RotateCcw, section: 'MY TEAMS' },
{ id: 'roadmap', label: 'Roadmap', icon: Map, section: 'MY TEAMS' },
{ id: 'backlog', label: 'Backlog', icon: Archive, section: 'MY TEAMS' },
{ id: 'q3-planning', label: 'Q3 Planning', icon: Star, section: 'FAVORITES' },
{ id: 'bug-fixes', label: 'Bug Fixes', icon: Bug, section: 'FAVORITES' },
{ id: 'design-system', label: 'Design System', icon: Layers, section: 'FAVORITES' },
];
const BOTTOM_ITEMS: NavItem[] = [
{ id: 'settings', label: 'Settings', icon: Settings, section: 'BOTTOM' },
{ id: 'invite', label: 'Invite Members', icon: UserPlus, section: 'BOTTOM' },
];
type SidebarProps = {
activeId: string;
onSelect: (id: string) => void;
collapsed: boolean;
onToggleCollapse: () => void;
};
export function SidebarNavigation({ activeId, onSelect, collapsed, onToggleCollapse }: SidebarProps) {
const sections = ['WORKSPACE', 'MY TEAMS', 'FAVORITES'];
return (
<div className={cn(
'flex flex-col h-full bg-[#0f0f0f] border-r border-white/[0.06] transition-all duration-300 ease-in-out shrink-0',
collapsed ? 'w-[52px]' : 'w-[240px]'
)}>
<div className={cn(
'flex items-center h-12 border-b border-white/[0.06] px-3',
collapsed ? 'justify-center' : 'justify-between'
)}>
<div className={cn('flex items-center gap-2 min-w-0', collapsed && 'justify-center')}>
<div className="w-5 h-5 rounded bg-indigo-500 flex items-center justify-center shrink-0">
<span className="text-[10px] font-bold text-white">A</span>
</div>
{!collapsed && <span className="text-sm font-semibold text-white truncate">Acme Corp</span>}
</div>
{!collapsed && <ChevronDown className="w-3.5 h-3.5 text-zinc-600 shrink-0" />}
</div>
<div className="flex-1 overflow-y-auto py-2">
{sections.map((section) => {
const items = NAV_ITEMS.filter((item) => item.section === section);
return (
<div key={section} className="mb-3">
{!collapsed && (
<p className="px-3 py-1 text-[10px] font-semibold tracking-widest text-zinc-600 uppercase">
{section}
</p>
)}
{items.map((item) => (
<NavItemButton
key={item.id}
item={item}
isActive={activeId === item.id}
collapsed={collapsed}
onClick={() => onSelect(item.id)}
/>
))}
</div>
);
})}
</div>
<div className="border-t border-white/[0.06] py-2">
{BOTTOM_ITEMS.map((item) => (
<NavItemButton
key={item.id}
item={item}
isActive={activeId === item.id}
collapsed={collapsed}
onClick={() => onSelect(item.id)}
/>
))}
<button
onClick={onToggleCollapse}
className={cn(
'flex items-center gap-2.5 px-3 py-2 mx-1 rounded-md text-zinc-600 hover:text-zinc-300 hover:bg-white/[0.04] transition-colors',
collapsed ? 'justify-center' : ''
)}
style={{ width: 'calc(100% - 8px)' }}
title={collapsed ? 'Expand' : 'Collapse'}
>
{collapsed ? (
<ChevronRight className="w-3.5 h-3.5 shrink-0" />
) : (
<><ChevronLeft className="w-3.5 h-3.5 shrink-0" /><span className="text-xs">Collapse</span></>
)}
</button>
</div>
</div>
);
}
function NavItemButton({ item, isActive, collapsed, onClick }: {
item: NavItem; isActive: boolean; collapsed: boolean; onClick: () => void;
}) {
const Icon = item.icon;
return (
<button
onClick={onClick}
title={collapsed ? item.label : undefined}
className={cn(
'flex items-center gap-2.5 px-3 py-1.5 mx-1 rounded-md text-left transition-colors',
collapsed ? 'justify-center' : '',
isActive ? 'bg-white/[0.08] text-white' : 'text-zinc-500 hover:text-zinc-300 hover:bg-white/[0.04]'
)}
style={{ width: 'calc(100% - 8px)' }}
>
<Icon className="w-3.5 h-3.5 shrink-0" />
{!collapsed && (
<>
<span className="flex-1 text-xs font-medium">{item.label}</span>
{item.count !== undefined && (
<span className="text-[10px] font-semibold text-zinc-400 bg-zinc-800 rounded px-1.5 py-0.5 shrink-0">
{item.count}
</span>
)}
</>
)}
</button>
);
}
// Usage demo
export function SidebarNavigationDemo() {
const [activeId, setActiveId] = useState('inbox');
const [collapsed, setCollapsed] = useState(false);
return (
<div className="flex h-[500px] bg-[#0a0a0a] rounded-xl border border-white/[0.06] overflow-hidden">
<SidebarNavigation
activeId={activeId}
onSelect={setActiveId}
collapsed={collapsed}
onToggleCollapse={() => setCollapsed((v) => !v)}
/>
<div className="flex-1 flex items-center justify-center">
<p className="text-sm text-zinc-600">Select an item from the sidebar</p>
</div>
</div>
);
}Unlock to copy
Free access to all patterns