Sidebar Navigation

A collapsible sidebar navigation inspired by Linear. Includes grouped nav items, active states, count badges, and smooth collapse animation.

Navigation & SidebarsInspired by Linear
Live PreviewInteractive
A
Acme Corp

WORKSPACE

MY TEAMS

FAVORITES

Select an item from the sidebar

SidebarNavigation.tsx
// 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