Workspace switcherDesign system
Product

Workspace switcher

Workspace selector with plan labels, default selection, settings, and creation actions.

npx context-ds add workspace-switcher
In the product

Usage

Use it for

The sidebar header.

Dependencies: dropdown-menu, tooltip, lucide-react

Example

examples/workspace-switcher.tsx
"use client";import { useState } from "react";import { WorkspaceSwitcher } from "@/components/product/workspace-switcher";import { ExampleMessage, ExampleNameDialog } from "./demo-feedback";export function Product() {  const [ws, setWs] = useState("acme");  const [def, setDef] = useState("acme");  const [editing, setEditing] = useState<string | null>(null);  const [message, setMessage] = useState("");  const [workspaces, setWorkspaces] = useState([{ id: "acme", name: "Acme", plan: "Pro" }, { id: "personal", name: "Personal", plan: "Free" }, { id: "labs", name: "Acme Labs", plan: "Starter" }]);  return <div className="grid w-64 gap-4">    <WorkspaceSwitcher value={ws} defaultId={def} onValueChange={setWs} onMakeDefault={setDef} onOpenSettings={setEditing} onCreate={() => setEditing("new")} workspaces={workspaces} />    <ExampleMessage>{message}</ExampleMessage>    <ExampleNameDialog title={editing === "new" ? "Create organization" : "Organization settings"} open={editing !== null} onOpenChange={(open) => { if (!open) setEditing(null); }} initial={workspaces.find((w) => w.id === editing)?.name} onSave={(name) => {      if (editing === "new") { const id = `workspace-${workspaces.length}`; setWorkspaces((all) => [...all, { id, name, plan: "Free" }]); setWs(id); setMessage(`${name} created.`); }      else { setWorkspaces((all) => all.map((w) => w.id === editing ? { ...w, name } : w)); setMessage("Organization name saved."); }    }} />  </div>;}

Source

components/product/workspace-switcher.tsx
"use client";import * as React from "react";import { ChevronsUpDown, Plus, Settings, Star } from "lucide-react";import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuLabel, DropdownMenuSeparator, DropdownMenuTrigger } from "@/components/ui/dropdown-menu";import { Tooltip, TooltipContent, TooltipTrigger } from "@/components/ui/tooltip";import { cn } from "@/lib/utils";export type Workspace = { id: string; name: string; plan?: string; logo?: string };/** The workspace's logo, or its initial on a black tile. */export function WorkspaceMark({ workspace, size = "sm" }: { workspace?: Workspace; size?: "sm" | "md" }) {  const sizeClass = size === "sm" ? "size-4 rounded-icon text-initial" : "size-5 rounded-logo text-counter";  if (workspace?.logo) return <img src={workspace.logo} alt="" className={cn("shrink-0 object-cover", sizeClass)} />;  return <span className={cn("grid shrink-0 place-items-center bg-black font-medium text-white", sizeClass)}>{(workspace?.name || "H").trim().charAt(0).toUpperCase()}</span>;}// The workspace at the top of the sidebar: its mark and name in medium, up-down chevrons after. The menu lists your// default workspace, then the others (each with its plan tag; hover one for the star that makes it your default), the// current one with a settings button, and "Create organization" at the foot.function WorkspaceSwitcher({  workspaces,  value,  defaultId,  onValueChange,  onMakeDefault,  onOpenSettings,  onCreate,  createLabel = "Create organization",}: {  workspaces: Workspace[];  value: string;  /** The default workspace (listed first, under its own label); the first workspace if not given. */  defaultId?: string;  onValueChange: (id: string) => void;  onMakeDefault?: (id: string) => void;  onOpenSettings?: (id: string) => void;  onCreate?: () => void;  createLabel?: string;}) {  const [open, setOpen] = React.useState(false);  const current = workspaces.find((w) => w.id === value) ?? workspaces[0];  const fallback = workspaces.find((w) => w.id === defaultId) ?? workspaces[0];  const others = workspaces.filter((w) => w.id !== fallback?.id);  const name = current?.name || "Home";  const row = (w: Workspace, canMakeDefault: boolean) => {    const isCurrent = w.id === value;    return (      <DropdownMenuItem        key={w.id}        onSelect={() => onValueChange(w.id)}        aria-current={isCurrent ? "page" : undefined}        className={cn("group/org h-9 pr-1.5", isCurrent ? "bg-neutral-5 data-[highlighted]:bg-neutral-5" : "")}      >        <WorkspaceMark workspace={w} size="md" />        <span className="flex min-w-0 flex-1 items-center gap-1.5">          <span className={cn("truncate", isCurrent ? "text-fg" : "text-fg-muted group-hover/org:text-fg")}>{w.name || "Home"}</span>          {w.plan && <span className="shrink-0 rounded-icon bg-blue-10 px-1.5 py-px text-counter leading-badge text-fg-brand">{w.plan}</span>}        </span>        {canMakeDefault && onMakeDefault && (          <button            type="button"            aria-label={`Make ${w.name} your default organization`}            title="Make default"            onPointerDown={(e) => e.stopPropagation()}            onPointerUp={(e) => e.stopPropagation()}            onKeyDown={(e) => e.stopPropagation()}            onClick={(e) => {              e.preventDefault();              e.stopPropagation();              onMakeDefault(w.id);            }}            className="grid size-6 place-items-center rounded-tag text-fg-subtle opacity-0 transition-[color,opacity] duration-150 group-hover/org:opacity-100 group-focus-within/org:opacity-100 group-data-[highlighted]/org:opacity-100 hover:text-fg focus-visible:opacity-100 pointer-coarse:opacity-100"          >            <Star strokeWidth={1.5} />          </button>        )}        {isCurrent && onOpenSettings && (          <Tooltip>            <TooltipTrigger asChild>              <button                type="button"                aria-label="Organization settings"                onPointerDown={(e) => e.stopPropagation()}                onPointerUp={(e) => e.stopPropagation()}                onKeyDown={(e) => e.stopPropagation()}                onClick={(e) => {                  e.preventDefault();                  e.stopPropagation();                  setOpen(false);                  onOpenSettings(w.id);                }}                className="grid size-6 place-items-center rounded-tag border border-line bg-surface text-fg-subtle transition-colors duration-150 hover:border-line-strong hover:text-fg"              >                <Settings strokeWidth={1.5} />              </button>            </TooltipTrigger>            <TooltipContent side="top">Organization settings</TooltipContent>          </Tooltip>        )}      </DropdownMenuItem>    );  };  return (    <DropdownMenu open={open} onOpenChange={setOpen}>      <DropdownMenuTrigger asChild>        <button          type="button"          aria-label={`${name}, switch organization`}          className="flex h-8 max-w-full min-w-0 items-center gap-2 rounded-control pr-2 pl-3 text-body-sm text-fg/80 outline-none transition-colors duration-150 hover:bg-wash-hover focus-visible:ring-2 focus-visible:ring-focus-ring data-[state=open]:bg-wash-hover"        >          <WorkspaceMark workspace={current} />          <span className="truncate text-left font-medium" title={name}>            {name}          </span>          <ChevronsUpDown className="-ml-0.5 size-3.5 shrink-0 text-fg-subtle" strokeWidth={1.5} />        </button>      </DropdownMenuTrigger>      <DropdownMenuContent align="start" className="max-h-[min(70vh,var(--radix-dropdown-menu-content-available-height))] w-54 max-w-[calc(100vw-2rem)] gap-0 overflow-y-auto">        <DropdownMenuLabel className="text-body-sm">Default organization</DropdownMenuLabel>        {fallback && row(fallback, false)}        {others.length > 0 && (          <>            <DropdownMenuLabel className="mt-1 text-body-sm">Other organizations</DropdownMenuLabel>            <div className="flex flex-col gap-0.5">{others.map((w) => row(w, true))}</div>          </>        )}        {onCreate && (          <>            <DropdownMenuSeparator />            <DropdownMenuItem onSelect={onCreate}>              <Plus strokeWidth={1.5} />              {createLabel}            </DropdownMenuItem>          </>        )}      </DropdownMenuContent>    </DropdownMenu>  );}export { WorkspaceSwitcher };

Reference: context-dashboard c0ddf7a src/components/shell/org-switcher.tsx.