Team switcher
ProA workspace switcher like Linear, Vercel or Slack. The trigger shows the current workspace's logo and plan. The menu has search, your role in each workspace and a Cmd+1 to 9 shortcut for each, and lets you create or join a workspace inline. A highlight glides between rows, and switching crossfades the trigger through a short loading state.
- Switching between organizations, teams or workspaces in a multi-tenant SaaS app.
- An account or project picker that needs search, roles and keyboard shortcuts.
- Letting people create a new workspace or accept an invite without leaving the current page.
- Picking from a short static list of options in a form: use a select or segmented control.
- Global navigation across pages and commands: use the command palette block.
- Switching between two or three views of the same data: use tabs.
Installation
Pro source and install commands unlock with a Pro plan.
import { useState } from "react";import { TeamSwitcher } from "@/components/arc/team-switcher/team-switcher"; const workspaces = [ { id: "acme", name: "Acme", plan: "Enterprise", members: 214, role: "Admin" }, { id: "acme-labs", name: "Acme Labs", plan: "Pro", members: 12, role: "Owner" },]; export function AppHeader() { const [workspaceId, setWorkspaceId] = useState("acme"); return ( <TeamSwitcher workspaces={workspaces} value={workspaceId} onValueChange={setWorkspaceId} onSwitch={(workspace, signal) => api.session.switch(workspace.id, { signal })} onCreateWorkspace={async (name, signal) => api.workspaces.create({ name }, { signal })} onJoinWorkspace={async (invite, signal) => api.invites.accept(invite, { signal })} /> );}API reference
2 parts. The first is the root.
TeamSwitcher
A workspace switcher: a trigger with the current workspace's logo and plan, and a menu with search, roles, Cmd/Ctrl+number shortcuts, and inline create and join views. With no props it switches between six sample workspaces and simulates every action.
workspacesWorkspace[]sampleWorkspacesThe workspaces you belong to: { id, name, plan?, members?, role?, logo?, tile?: { background, color? } }. The first nine get Cmd/Ctrl+1 to 9. Size a custom logo in em; the tile sets font-size to its own size.valuestring–The active workspace id. Pair with onValueChange.defaultValuestringfirst workspaceThe starting workspace when uncontrolled.onValueChange(id: string, workspace: Workspace) => void–Called once a switch has finished loading.onSwitch(workspace: Workspace, signal: AbortSignal) => Promise<void>–Loads the workspace. The trigger crossfades to the new workspace with a switching state until it resolves; a rejection reverts and shows the error's message. A newer switch aborts the older one. Simulated (650ms) when left out.onCreateWorkspace(name: string, signal: AbortSignal) => Promise<Workspace | void>–Creates a workspace. Return it to add it and switch to it; throw an Error to show its message under the field. Simulated when left out.onJoinWorkspace(invite: string, signal: AbortSignal) => Promise<Workspace | void>–Joins from an invite link or code. Return the workspace to switch to it; throw an Error to show its message. Simulated when left out, with sample invites for Resend and Notion.openboolean–Controls the menu. Pair with onOpenChange.defaultOpenbooleanfalseStarts with the menu open, without moving focus.onOpenChange(open: boolean) => void–Called when the menu opens or closes.globalShortcutsbooleanfalseListens for Cmd/Ctrl+1 to 9 anywhere on the page. Off, the shortcuts work while focus is inside the switcher.simulatedDelaynumber650How long the simulated switch takes, in milliseconds.disabledbooleanfalseDisables the trigger.classNamestring–Class on the root. The root is up to 360px wide and fills narrower containers.WorkspaceTile
The workspace avatar on its own: the brand mark on its brand color, or the first letter of the name.
workspaceRequiredWorkspace–The workspace to draw.sizenumber28Tile size in pixels. The corner radius and mark scale with it.- EnterorSpaceorArrowDown
- On the trigger, opens the menu and moves focus to the search field.
- Type
- Filters workspaces by name, plan or role. With no match, Enter opens Create workspace with the name filled in.
- ArrowDownorArrowUp
- Moves the highlight through workspaces and actions, wrapping at the ends.
- HomeorEnd
- Jumps to the first or last item when the search is empty.
- Enter
- Switches to the highlighted workspace, or opens Create workspace or Join with invite.
- Cmd/Ctrl+1 to 9
- Switches to the workspace at that place in the list. Works while focus is in the switcher, or anywhere with globalShortcuts.
- Escape
- Goes back from create or join, then clears the search, then closes and returns focus to the trigger.
- The trigger is a button with aria-haspopup="dialog", aria-expanded and aria-busy while switching; its label reads the workspace and its plan.
- The search field is a combobox that owns a listbox and points at the highlighted row with aria-activedescendant, so focus never leaves the field while navigating.
- The current workspace is aria-selected and marked with a check icon, not color alone. Errors pair an icon with text.
- A polite live region announces switching, the result, and when an action was simulated ("Simulated, nothing was sent").
- Closing with Escape or by choosing returns focus to the trigger. Tabbing out or clicking outside closes the menu.
- The menu fades and scales in from 98% over 180ms and leaves in 120ms.
- One highlight glides between rows with a bounce-free spring (visualDuration 0.24) and appears in place instead of sliding in from the top.
- Switching crossfades the trigger's logo, name and plan line (100ms out, 160ms in, 2px y) through a shimmer label and a spinner. Nothing resizes.
- The create and join views crossfade inside a fixed-height panel with a 6px horizontal shift that follows the navigation direction.
- Reduced motion keeps the crossfades and feedback as plain fades and drops the glide, shifts and scale.
- The root is up to 360px wide and fills narrower containers, so it holds at 360px and 390px viewports.
- Names and plan lines truncate with an ellipsis; roles and shortcuts keep their width.
- The list shows six rows and scrolls beyond that without a visible scrollbar; the panel height never changes.
- Rows are 48px tall for touch, and pointer and keyboard drive the same highlight.
- One motion value pair drives the highlight; rows do not animate individually.
- Filtering is a plain in-memory match on name, plan and role, fine for hundreds of workspaces.
- Marks are inline SVG; no images load.
Notes for AI
Give your coding assistant the Markdown reference instead of screenshots.
- Use it at the top of an app sidebar or in a header for multi-tenant SaaS: organizations, teams, projects or stores.
- Pass real workspaces with logo and tile when the tenant has a brand mark; otherwise the tile falls back to a letter avatar.
- Wire onSwitch to whatever loads the tenant (route prefetch, session switch); keep the promise short, the trigger shows a loading state meanwhile.
- Return the created or joined workspace from onCreateWorkspace or onJoinWorkspace and also add it to the workspaces prop; the switcher dedupes by id.
- The menu is absolutely positioned below the trigger. Leave about 450px below it or place it where the panel can overflow.
The full library index for assistants is at /llms.txt.