# Workspace Switcher

> A React workspace switcher for organizations, teams or projects: logos, plan badges, search, groups, mod+1…9 shortcuts and a create row.

- Type: React component (Navigation)
- Access: Pro (the source needs a Wingo UI Pro plan, see https://wingo-ui.com/pricing)
- Version: 1.0.0
- Install: `npx wingo-ui@latest add workspace-switcher`
- Docs: https://wingo-ui.com/components/workspace-switcher

## Interaction

Click the trigger (a card with the logo, name and plan in a sidebar header, a compact logo and name in a top bar, or the logo alone in a rail, with a tooltip) and a list of workspaces grows out of it, grouped (Personal, Teams), each row with its logo, plan badge, description and a check on the current one. Pick one and the trigger's logo and name crossfade to it; when the switch loads data (onValueChange returns a promise) the row spins and the list stays open until it finishes. Disabled workspaces are listed but cannot be picked. With more than six workspaces a search row filters by name, plan, city or keywords (diacritics do not matter) and bolds the match. Under the list sit the Create workspace row and your own actions. Keyboard: Enter, Space, Down or Up on the trigger opens it; Up / Down move, Home / End jump, Enter picks, typing filters (or jumps by first letter without search), Tab moves from the search or the list to the footer rows, leaving the footer closes, Escape closes and focus returns to the trigger. With shortcuts on, Mod+1 … Mod+9 switch to the first nine workspaces from anywhere (the hints show in the rows and the switch is announced), and a shortcut prop opens it. In a collapsed sidebar (data-collapsed="true" on a group/sidebar parent) the card shrinks to the logo. On a phone it is a bottom sheet with a title, the search under it, 48px rows and the footer rows above the safe area; drag it down or tap outside to close.

## Installation

```bash
npx wingo-ui@latest add workspace-switcher
```

Pro item: run `npx wingo-ui@latest login` once with a Pro account. The CLI adds the files, their dependencies and a version record for updates.

With shadcn: `npx shadcn@latest add @wingo-ui/workspace-switcher` (needs the @wingo-ui registry with your API key in components.json)

Files:

- `components/ui/workspace-switcher.tsx`

npm dependencies: `npm i @radix-ui/react-popover lucide-react motion`

Wingo UI dependencies (installed automatically):

- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))
- [Badge](https://wingo-ui.com/components/badge) ([Markdown](https://wingo-ui.com/components/badge.md))
- [Drawer](https://wingo-ui.com/components/drawer) ([Markdown](https://wingo-ui.com/components/drawer.md))
- [Dropdown Menu](https://wingo-ui.com/components/dropdown-menu) ([Markdown](https://wingo-ui.com/components/dropdown-menu.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Spinner](https://wingo-ui.com/components/spinner) ([Markdown](https://wingo-ui.com/components/spinner.md))
- [Tooltip](https://wingo-ui.com/components/tooltip) ([Markdown](https://wingo-ui.com/components/tooltip.md))
- [useControllableState](https://wingo-ui.com/components/use-controllable-state) ([Markdown](https://wingo-ui.com/components/use-controllable-state.md))
- [useMediaQuery](https://wingo-ui.com/components/use-media-query) ([Markdown](https://wingo-ui.com/components/use-media-query.md))
- [useShortcut](https://wingo-ui.com/components/use-shortcut) ([Markdown](https://wingo-ui.com/components/use-shortcut.md))
- [Motion presets](https://wingo-ui.com/components/motion) ([Markdown](https://wingo-ui.com/components/motion.md))
- [Navigation](https://wingo-ui.com/components/navigation) ([Markdown](https://wingo-ui.com/components/navigation.md))
- [Text match](https://wingo-ui.com/components/text-match) ([Markdown](https://wingo-ui.com/components/text-match.md))
- [UIProvider](https://wingo-ui.com/components/ui-config) ([Markdown](https://wingo-ui.com/components/ui-config.md))

## Usage

```tsx
"use client"

import { Settings, UserPlus } from "lucide-react"
import { WorkspaceSwitcher, type Workspace } from "@/components/ui/workspace-switcher"

const workspaces: Workspace[] = [
  { id: "emma", name: "Emma Carter", logo: "/avatars/emma.jpg", plan: "Free", group: "Personal" },
  { id: "north-studio", name: "North Studio LLC", plan: "Pro", description: "12 members · New York", group: "Teams" },
  { id: "summit", name: "Summit Logistics", plan: "Business", description: "48 members · Boston", group: "Teams" },
]

export function SidebarHeader({ onSwitch }: { onSwitch: (id: string) => Promise<void> }) {
  return (
    <WorkspaceSwitcher
      workspaces={workspaces}
      defaultValue="north-studio"
      onValueChange={(id) => onSwitch(id)}
      shortcuts
      onCreate={() => console.log("new workspace")}
      createLabel="Create workspace"
      actions={[
        { label: "Workspace settings", icon: <Settings />, href: "/settings" },
        { label: "Invite members", icon: <UserPlus />, onSelect: () => console.log("invite") },
      ]}
      searchPlaceholder="Search workspaces"
      emptyText="No workspaces found"
      sheetTitle="Switch workspace"
    />
  )
}
```

## Props

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `workspaces?` | `Workspace[]` | `"[{ id: \"personal\", name: \"Personal\", plan: \"Free\", description: \"Just you\", group: \"Personal\" }, { id: \"acme\", name: \"Acme Inc\", plan: \"Pro\", description: \"12 members\", group: \"Teams\" }, { id: \"northwind\", name: \"Northwind\", plan: \"Business\", description: \"48 members\", group: \"Teams\" }]"` | The workspaces: { id, name, logo?: string (image url) \| ReactNode (an icon on a color from the name), description?, plan?, group?, disabled?, keywords? }. Without a logo (or when the image fails) the initials show on a color from the name, legal forms left out (North Studio LLC reads NS). The logo alone is exported as WorkspaceLogo ({ workspace, size, radius, variant }) for your own rows. |
| `value?` | `string` |  | The current workspace id (controlled). An id that is not in the list shows the placeholder. |
| `defaultValue?` | `string` | `""` | The starting workspace id while uncontrolled. Empty: the first enabled workspace; an unknown id shows the placeholder. |
| `onValueChange?` | `(id: string, workspace: Workspace) => (void \| Promise<unknown>)` |  | Fires on a pick or a Mod+1…9 switch. Return a promise and the row shows a spinner while the list stays open; the switch commits when it resolves (a rejection keeps the current workspace). |
| `variant?` | `"card" \| "compact" \| "icon"` | `"card"` | card: logo, name, plan / description and an up-down chevron filling the width (sidebar headers; logo only in a collapsed sidebar). compact: logo, name and a chevron (top bars). icon: the logo alone with a tooltip (rails). |
| `size?` | `"sm" \| "md" \| "lg"` | `"md"` | card and icon: 40 / 48 / 56px with a 24 / 32 / 40px logo. compact: 32 / 40 / 48px, like the buttons next to it in a top bar. 44px targets on touch. |
| `logoRadius?` | `"sm" \| "md" \| "full"` | `"md"` | Logo corners, proportional to its size: organization logos are rounded squares, full makes circles. |
| `logoVariant?` | `"solid" \| "soft"` | `"solid"` | The look of logos without an image: solid white initials on a color from the name, or a soft tint with deep initials. |
| `meta?` | `"auto" \| "plan" \| "description" \| "none"` | `"auto"` | The card's second line: the plan and the description joined with a dot (auto), one of them, or nothing. |
| `search?` | `"auto" \| "always" \| "never"` | `"auto"` | The search row on top of the list. auto: above 6 workspaces. On touch screens it waits for a tap so the keyboard does not cover the list (unless always). |
| `searchPlaceholder?` | `string` | `"Find a workspace"` | The search row's placeholder. |
| `emptyText?` | `string` | `"No workspaces found"` | Shown when the search matches nothing. |
| `groupBy?` | `boolean` | `true` | Groups rows under their workspace.group label, in the order the groups first appear. |
| `shortcuts?` | `boolean` | `false` | Binds Mod+1 … Mod+9 to the first nine enabled workspaces while mounted (Slack, Linear), shows the hints in the rows and announces the switch. |
| `onCreate?` | `() => void` |  | Shows the Create workspace row under the list. |
| `createLabel?` | `string` | `"Create workspace"` | The create row's label. |
| `actions?` | `MenuItemEntry[]` |  | More footer rows, in dropdown-menu's item format: { label, icon, shortcut, badge, href, onSelect, tone, disabled, keepOpen }. href rows render through NavLink; shortcuts run while it is open. |
| `placeholder?` | `string` | `"Select a workspace"` | The trigger's text while no workspace is selected. |
| `contentWidth?` | `number` | `288` | The list's width in px; never narrower than the trigger, never wider than the screen. |
| `maxHeight?` | `number` | `460` | The list's maximum height in px, capped by the room on screen; the list scrolls and the footer stays. |
| `side?` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | Where the list opens; it flips when there is no room. |
| `align?` | `"start" \| "center" \| "end"` | `"start"` | Alignment against the trigger. |
| `sideOffset?` | `number` | `8` | Gap between the trigger and the list, in px. |
| `open?` | `boolean` |  | Controlled open state; pair it with onOpenChange. |
| `defaultOpen?` | `boolean` | `false` | Starts open (uncontrolled). |
| `onOpenChange?` | `(open: boolean) => void` |  | Fires when the list or the sheet opens or closes. |
| `disabled?` | `boolean` | `false` | Dims the trigger and keeps it closed. |
| `responsive?` | `boolean` | `true` | Below 768px the list becomes a bottom sheet. Off: the popover on every screen. |
| `sheetTitle?` | `string` | `"Switch workspace"` | The phone sheet's title, and the list's accessible name. |
| `shortcut?` | `string` | `""` | A key combo in mod+k notation that opens and closes it from anywhere (e.g. mod+o); the trigger gets aria-keyshortcuts. Empty binds nothing. |
| `triggerLabel?` | `(name: string) => string` | `"(name) => \"Workspace: \" + name"` | The icon trigger's accessible name and tooltip. |
| `switchingLabel?` | `string` | `"Switching"` | What screen readers hear for the row spinner while a switch loads. |
| `container?` | `HTMLElement \| null` |  | Portal target for the list and the sheet (default document.body). |
| `classNames?` | `Partial<Record<"trigger" \| "logo" \| "name" \| "meta" \| "chevron" \| "content" \| "search" \| "list" \| "option" \| "optionLogo" \| "optionName" \| "optionDescription" \| "optionPlan" \| "optionShortcut" \| "empty" \| "footer" \| "create" \| "action", string>>` |  | Extra classes for the inner parts. |
| `className?` | `string` |  | Extra classes for the trigger, merged last. Other props (id, aria-*, data-*, handlers) are spread onto it. |

## Parts

### WorkspaceLogo (component)

The organization logo on its own: the image, else the initials (or the given icon) on a color from the name. Use it in your own rows, headers or settings pages so they match the switcher.

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `workspace` | `Workspace \| null` |  | The workspace to draw; null shows the empty placeholder. |
| `size?` | `number` | `32` | Size in px. |
| `radius?` | `"sm" \| "md" \| "full"` | `"md"` | Corner shape. |
| `variant?` | `"soft" \| "solid"` | `"solid"` | A filled color or a soft tint behind the initials. |
| `className?` | `string` |  | Extra classes. |

### workspaceInitials (function)

The initials of an organization: the first letters of its first two words, legal forms such as LLC, Inc. or GmbH left out, so North Studio LLC reads NS. Signature: (name: string) => string.

### Workspace (type)

One workspace: { id, name, logo?, description?, plan?, group?, disabled?, keywords? }.

## Examples

- **Card, compact and icon**: The sidebar header card, the top-bar compact trigger and the logo in a 64px rail with a tooltip.
- **14 workspaces with search**: Search appears above six workspaces; matches are bold and a disabled workspace stays listed.
- **Mod+1 … Mod+9**: shortcuts binds the first nine workspaces and shows the hints in the rows.
- **Create flow**: onCreate opens a “New workspace” dialog and the new workspace is selected.
- **Footer actions**: Workspace settings, Invite members and a danger row under the list; round logos, no groups.
- **Collapsed sidebar**: The card follows the sidebar's data-collapsed attribute: the logo alone in the rail.
- **Sizes**: sm, md and lg cards with soft logos.
- **Phone sheet**: What the list becomes below 768px.

## See also

- [User Menu](https://wingo-ui.com/components/user-menu) ([Markdown](https://wingo-ui.com/components/user-menu.md))
- [Sidebar](https://wingo-ui.com/components/sidebar) ([Markdown](https://wingo-ui.com/components/sidebar.md))
- [Select](https://wingo-ui.com/components/select) ([Markdown](https://wingo-ui.com/components/select.md))
- [Listbox](https://wingo-ui.com/components/listbox) ([Markdown](https://wingo-ui.com/components/listbox.md))
- [Avatar](https://wingo-ui.com/components/avatar) ([Markdown](https://wingo-ui.com/components/avatar.md))

---

Canonical: https://wingo-ui.com/components/workspace-switcher
More components: https://wingo-ui.com/components · Index for agents: https://wingo-ui.com/llms.txt
