Workspace Switcher

A workspace-switcher pill that opens a dropdown of all workspaces — colored avatars with initials, member counts, an active-workspace checkmark, and a dashed Create workspace action below a divider. Inspired by Slack.

Navigation & SidebarsInspired by Slack
Live PreviewInteractive
Slack · Sidebar workspace switcher

Click the workspace pill to open · Active workspace is checked · + creates a new workspace

WorkspaceSwitcher.tsx
// Dependencies: react ^18, lucide-react
import React, { useState } from 'react';
import { ChevronDown, Check, Plus } from 'lucide-react';

function cn(...classes: (string | false | null | undefined)[]) {
  return classes.filter(Boolean).join(' ');
}

type Workspace = {
  id: string;
  name: string;
  initials: string;
  color: string;
  members: number;
};

const WORKSPACES: Workspace[] = [
  { id: 'acme', name: 'Acme Corp', initials: 'AC', color: '#6366f1', members: 42 },
  { id: 'beta', name: 'Beta Studio', initials: 'BS', color: '#f59e0b', members: 8 },
  { id: 'demo', name: 'Demo Workspace', initials: 'DW', color: '#10b981', members: 3 },
  { id: 'side', name: 'Side Project', initials: 'SP', color: '#ec4899', members: 1 },
];

export function WorkspaceSwitcher() {
  const [activeId, setActiveId] = useState<string>('acme');
  const [open, setOpen] = useState(false);

  const active = WORKSPACES.find((w) => w.id === activeId) ?? WORKSPACES[0];

  return (
    <div className="w-full max-w-[300px]">
      <div className="relative">
        <button
          onClick={() => setOpen((v) => !v)}
          onBlur={() => setTimeout(() => setOpen(false), 120)}
          className={cn(
            'w-full flex items-center gap-3 px-3 py-2.5 rounded-lg bg-zinc-900 border border-white/[0.08] hover:bg-zinc-800/80 transition-colors',
            open && 'bg-zinc-800/80'
          )}
        >
          <WorkspaceAvatar workspace={active} size={32} />
          <div className="flex-1 min-w-0 text-left">
            <p className="text-sm font-semibold text-white truncate">{active.name}</p>
            <p className="text-[11px] text-zinc-500 truncate">
              {active.members} {active.members === 1 ? 'member' : 'members'}
            </p>
          </div>
          <ChevronDown
            className={cn(
              'w-4 h-4 text-zinc-500 transition-transform shrink-0',
              open && 'rotate-180'
            )}
          />
        </button>

        {open && (
          <div className="absolute left-0 right-0 top-full mt-1 z-50 bg-[#1a1a1a] border border-[#2a2a2a] rounded-lg shadow-2xl py-1">
            <div className="px-2 pt-1 pb-0.5">
              <p className="px-1 text-[10px] font-semibold tracking-widest text-zinc-500 uppercase">
                Workspaces
              </p>
            </div>
            {WORKSPACES.map((ws) => {
              const isActive = ws.id === activeId;
              return (
                <button
                  key={ws.id}
                  onMouseDown={(e) => {
                    e.preventDefault();
                    setActiveId(ws.id);
                    setOpen(false);
                  }}
                  className={cn(
                    'flex items-center gap-2.5 px-2 py-1.5 mx-1 rounded-md text-left transition-colors',
                    isActive ? 'bg-white/[0.06]' : 'hover:bg-white/[0.04]'
                  )}
                  style={{ width: 'calc(100% - 8px)' }}
                >
                  <WorkspaceAvatar workspace={ws} size={24} />
                  <div className="flex-1 min-w-0">
                    <p className="text-[13px] font-medium text-zinc-200 truncate">{ws.name}</p>
                    <p className="text-[10px] text-zinc-500 truncate">
                      {ws.members} {ws.members === 1 ? 'member' : 'members'}
                    </p>
                  </div>
                  {isActive && <Check className="w-3.5 h-3.5 text-indigo-400 shrink-0" />}
                </button>
              );
            })}

            <div className="h-px bg-white/[0.06] my-1" />

            <button
              onMouseDown={(e) => {
                e.preventDefault();
                setOpen(false);
              }}
              className="flex items-center gap-2.5 px-2 py-1.5 mx-1 rounded-md text-left text-zinc-400 hover:bg-white/[0.04] hover:text-white transition-colors"
              style={{ width: 'calc(100% - 8px)' }}
            >
              <span className="w-6 h-6 rounded-md bg-zinc-800 border border-dashed border-white/[0.1] flex items-center justify-center shrink-0">
                <Plus className="w-3 h-3 text-zinc-500" />
              </span>
              <span className="text-[13px] font-medium">Create workspace</span>
            </button>
          </div>
        )}
      </div>
    </div>
  );
}

function WorkspaceAvatar({ workspace, size }: { workspace: Workspace; size: number }) {
  return (
    <span
      className="rounded-md flex items-center justify-center shrink-0"
      style={{ width: size, height: size, backgroundColor: workspace.color }}
    >
      <span className="font-bold text-white" style={{ fontSize: size <= 24 ? 10 : 12 }}>
        {workspace.initials}
      </span>
    </span>
  );
}

Unlock to copy

Free access to all patterns