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.
Click the workspace pill to open · Active workspace is checked · + creates a new workspace
// 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
Command Palette
Linear
Global Search with Results Grouping
Linear
Recent + Suggested Search
Raycast
Filter Builder (AND / OR)
Linear
Sidebar Navigation
Linear
Collapsible Nested Tree Nav
Notion
Tab Bar with Overflow Menu
Vercel
Breadcrumb with Dropdowns
Linear
Workspace Switcher
Slack
Deployment Status Card
Vercel
Empty State
Vercel
Slash Command Menu
Notion
Stats & Metrics Row
Vercel
Single Big Metric Card
Stripe
Confirmation Dialog
Linear
Slide-Over Panel
Stripe
Issue / Task Card
Linear
Activity Feed
Linear + Slack
Properties Panel
Notion + Linear
Inline Data Table
Notion + Linear