# WorkspaceSwitcher

> Organisation / team switcher for the top of the sidebar, with logo, plan line, an optional "Add workspace" item and room for extra menu items.

Source: https://docs.nasaqui.com/components/workspace-switcher

## Install

```bash
npx shadcn@latest add https://docs.nasaqui.com/r/workspace-switcher.json
```

Switches between organisations, teams or tenants inside one product. It sits first in the sidebar
header: a bordered control showing the active workspace's logo, name and plan, opening a menu of all
workspaces. On the collapsed rail it becomes a logo-only button and the menu opens to the side.

## When to use

- Users belong to more than one workspace, org or team.
- You want "Add workspace" and workspace-level links (Settings, Invite members) in the same menu.

## When not to use

- Moving between different products: use [`ProductSwitcher`](https://docs.nasaqui.com/components/product-switcher).
- Account, theme and sign-out: use [`UserMenu`](https://docs.nasaqui.com/components/user-menu).
- A plain select in a form: use a select field.

## Import

```tsx
import { WorkspaceSwitcher, type Workspace } from "@fadymondy/nasaq/web";
// inside this monorepo: "@nasaq/web"
```

## Quick start

```tsx
import { Sidebar, SidebarHeader, WorkspaceSwitcher, type Workspace } from "@fadymondy/nasaq/web";
import { useState } from "react";

const workspaces: Workspace[] = [
  { id: "3x1", name: "3x1", description: "Pro · 12 members" },
  { id: "personal", name: "Fady Mondy", description: "Personal" },
];

export function Header() {
  const [workspace, setWorkspace] = useState("3x1");
  return (
    <Sidebar>
      <SidebarHeader>
        <WorkspaceSwitcher workspaces={workspaces} value={workspace} onValueChange={setWorkspace} />
      </SidebarHeader>
    </Sidebar>
  );
}
```

It reads the rail state from [`AppShell`](https://docs.nasaqui.com/components/app-shell) and the locale from `NasaqProvider`.
Render it inside the shell's sidebar.

## Anatomy

```
WorkspaceSwitcher
├─ trigger                     data-slot="workspace-switcher"  (DropdownMenuTrigger; data-popup-open)
│  ├─ logo                     Avatar (square) from name/logoSrc, or your `logo`
│  ├─ name + description       hidden on the rail
│  └─ chevrons-up-down         decorative, hidden on the rail
└─ menu (DropdownMenuContent)
   ├─ label                    labels.heading
   ├─ item × n                 logo + name, check mark and aria-current on the active one
   ├─ separator                only with children or onCreate
   ├─ children                 your extra DropdownMenuItems
   └─ "Add workspace"          only with onCreate
```

## API

### `Workspace`

| Field | Type | Description |
| --- | --- | --- |
| `id` | `string` | Stable id. Compared with `value`. |
| `name` | `string` | Display name, already localised. |
| `description?` | `string` | Plan or role, e.g. "Pro" or "Owner". Shown under the name in the trigger. |
| `logo?` | `ReactNode` | A `ProductMark`, image… Shown in a bordered tile. Defaults to the initials avatar. |
| `logoSrc?` | `string` | Image for the default avatar when `logo` is not given. |

### `WorkspaceSwitcher`

| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| `workspaces` | `Workspace[]` | required | Options. Renders nothing when empty. |
| `value` | `string` | required | Id of the active workspace. An unknown id falls back to the first workspace. |
| `onValueChange` | `(id: string) => void` | required | Called when the user picks one. |
| `onCreate?` | `() => void` | none | Adds an "Add workspace" item. |
| `className?` | `string` | none | Classes for the trigger. |
| `labels?` | `{ heading?: string; create?: string }` | EN/AR built in | Menu heading ("Workspaces" / "مساحات العمل") and create item ("Add workspace" / "إضافة مساحة عمل"). |
| `children?` | `ReactNode` | none | Extra `DropdownMenuItem`s under the list (Settings, Invite members…). |

## Examples

### With a brand mark, create and extra items

```tsx
import { DropdownMenuItem, ProductMark, WorkspaceSwitcher, type Workspace } from "@fadymondy/nasaq/web";
import { Settings, UserPlus } from "lucide-react";
import { useState } from "react";

const workspaces: Workspace[] = [
  { id: "mahaam", name: "Mahaam", description: "Team · 5 members", logo: <ProductMark brand="mahaam" size={18} title="" /> },
  { id: "personal", name: "Fady Mondy", description: "Personal" },
];

export function Switcher() {
  const [value, setValue] = useState("mahaam");
  return (
    <WorkspaceSwitcher workspaces={workspaces} value={value} onValueChange={setValue} onCreate={() => console.log("create")}>
      <DropdownMenuItem><Settings />Workspace settings</DropdownMenuItem>
      <DropdownMenuItem><UserPlus />Invite members</DropdownMenuItem>
    </WorkspaceSwitcher>
  );
}
```

### Arabic

```tsx
import { WorkspaceSwitcher } from "@fadymondy/nasaq/web";

export const Ar = () => (
  <WorkspaceSwitcher
    workspaces={[{ id: "a", name: "ثلاثة في واحد", description: "احترافي · 12 عضوًا" }]}
    value="a"
    onValueChange={() => {}}
    labels={{ heading: "مساحات العمل", create: "إضافة مساحة عمل" }}
  />
);
```

## Accessibility

| Key | Action |
| --- | --- |
| `Enter` / `Space` / `↓` on trigger | Opens the menu. |
| `↑` `↓` | Move between workspaces and actions. |
| `Home` / `End` | First / last item. |
| `Enter` / `Space` | Choose the highlighted workspace or action. |
| `Esc` | Closes and returns focus to the trigger. |

- The trigger is a menu button. Expanded, its name comes from its text (name and description); on the rail it gets `aria-label` = the active workspace's name.
- The active item has `aria-current="true"`, medium weight and a check icon. Items show no shortcut hints (`Ctrl 1`…`9` clash with browser tab switching); to bind your own, use [`useRegisterCommands`](https://docs.nasaqui.com/components/commands).
- Logos are decorative beside the visible name.
- **Localise:** `labels`, workspace names and descriptions.

## RTL & i18n

- The trigger, list and create tile use logical properties and mirror. The menu opens from the inline start.
- On the rail the menu opens to `inline-end`, so it appears on the left in RTL.
- Built-in strings switch on the provider locale (`ar*` gets Arabic).
- The check mark sits at the inline end and is not mirrored.

## Styling & tokens

- Trigger: `border-border`, `bg-background/60`, hover `bg-nq-hover`, open `bg-nq-selected`, focus `nq-focus`. Menu uses the popover surface.
- Target `[data-slot=workspace-switcher]` and its `data-popup-open` attribute. Extend the trigger with `className`.

## Do / Don't

- **Do** put it first in `SidebarHeader`.
- **Do** show plan or role in `description`; it disambiguates similarly named workspaces.
- **Don't** use it to move between products (that is the product switcher).
- **Don't** put business logic here: `onValueChange` and `onCreate` are yours.

## Related

- [app-shell](https://docs.nasaqui.com/components/app-shell) · [user-menu](https://docs.nasaqui.com/components/user-menu)
- [product-switcher](https://docs.nasaqui.com/components/product-switcher) · [command-palette](https://docs.nasaqui.com/components/command-palette)

## Lab

https://docs.nasaqui.com/?path=/docs/components-navigation-workspace-switcher--docs

## Code

### React

```tsx
import { Sidebar, SidebarHeader, WorkspaceSwitcher, type Workspace } from "@fadymondy/nasaq/web";
import { useState } from "react";

const workspaces: Workspace[] = [
  { id: "3x1", name: "3x1", description: "Pro · 12 members" },
  { id: "personal", name: "Fady Mondy", description: "Personal" },
];

export function Header() {
  const [workspace, setWorkspace] = useState("3x1");
  return (
    <Sidebar>
      <SidebarHeader>
        <WorkspaceSwitcher workspaces={workspaces} value={workspace} onValueChange={setWorkspace} />
      </SidebarHeader>
    </Sidebar>
  );
}
```

### shadcn

```tsx
import { Sidebar, SidebarHeader } from "@/components/ui/app-shell";
import { WorkspaceSwitcher, type Workspace } from "@/components/ui/workspace-switcher";
import { useState } from "react";

const workspaces: Workspace[] = [
  { id: "3x1", name: "3x1", description: "Pro · 12 members" },
  { id: "personal", name: "Fady Mondy", description: "Personal" },
];

export function Header() {
  const [workspace, setWorkspace] = useState("3x1");
  return (
    <Sidebar>
      <SidebarHeader>
        <WorkspaceSwitcher workspaces={workspaces} value={workspace} onValueChange={setWorkspace} />
      </SidebarHeader>
    </Sidebar>
  );
}
```

### Vue

```vue
<script setup lang="ts">
import { ref } from "vue";
import { NqSidebar, NqSidebarHeader, NqWorkspaceSwitcher, type Workspace } from "@fadymondy/nasaq/vue";

const workspaces: Workspace[] = [
  { id: "3x1", name: "3x1", description: "Pro · 12 members" },
  { id: "personal", name: "Fady Mondy", description: "Personal" },
];
const workspace = ref("3x1");
</script>

<template>
  <NqSidebar>
    <NqSidebarHeader>
      <NqWorkspaceSwitcher v-model="workspace" :workspaces="workspaces" />
    </NqSidebarHeader>
  </NqSidebar>
</template>
```

### Blade

```blade
<div class="w-64" x-data="{ workspace: '3x1' }">
    <x-nq::workspace-switcher x-model="workspace" :workspaces="[
        ['id' => '3x1', 'name' => '3x1', 'description' => 'Pro · 12 members'],
        ['id' => 'personal', 'name' => 'Fady Mondy', 'description' => 'Personal'],
    ]" value="3x1" create />
</div>
```

### HTML + Alpine

```html
<div class="w-64" x-data="{ workspace: '3x1' }">
    <div data-slot="workspace-switcher-root" x-data="{ ws: '3x1', names: JSON.parse('{\u00223x1\u0022:\u00223x1\u0022,\u0022personal\u0022:\u0022Fady Mondy\u0022}') }" x-modelable="ws" x-model="workspace" class="contents">
<div data-slot="dropdown-menu" x-data="nqDropdownMenu(false)" x-modelable="open" class="contents"><button type="button" data-slot="workspace-switcher" x-bind="trigger" x-ref="trigger"
        x-bind:aria-label="typeof rail !== 'undefined' && rail && collapsed ? names[ws] : null"
        class="flex w-full items-center gap-2 rounded-control px-1.5 text-start outline-none transition-colors duration-150 ease-nq hover:bg-nq-hover data-popup-open:bg-nq-selected focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-nq-focus min-h-[calc(var(--spacing-nav-row)+12px)] border border-border bg-background/60 py-1 shadow-xs hover:border-nq-line-strong group-data-collapsed/sidebar:size-control group-data-collapsed/sidebar:justify-center group-data-collapsed/sidebar:px-0 group-data-collapsed/sidebar:border-0 group-data-collapsed/sidebar:bg-transparent group-data-collapsed/sidebar:shadow-none">
                    <span data-workspace="3x1" class="flex min-w-0 flex-1 items-center gap-2 group-data-collapsed/sidebar:flex-none"
                x-show="ws === $el.dataset.workspace" >
                <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-control">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="3x1" >3</span>
    </span>
                <span class="grid min-w-0 flex-1 group-data-collapsed/sidebar:hidden">
                    <span class="truncate text-label leading-snug text-foreground">3x1</span>
                    <span class="truncate text-caption leading-snug text-muted-foreground">Pro · 12 members</span>                </span>
            </span>
                    <span data-workspace="personal" class="flex min-w-0 flex-1 items-center gap-2 group-data-collapsed/sidebar:flex-none"
                x-show="ws === $el.dataset.workspace"  style="display: none" >
                <span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-8 text-caption rounded-control">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Fady Mondy" >FM</span>
    </span>
                <span class="grid min-w-0 flex-1 group-data-collapsed/sidebar:hidden">
                    <span class="truncate text-label leading-snug text-foreground">Fady Mondy</span>
                    <span class="truncate text-caption leading-snug text-muted-foreground">Personal</span>                </span>
            </span>
                <svg aria-hidden="true" class="size-4 shrink-0 text-muted-foreground group-data-collapsed/sidebar:hidden" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="m7 15 5 5 5-5"/>
  <path d="m7 9 5-5 5 5"/>
</svg>    </button>
    <template x-teleport="body">
    <div data-slot="dropdown-menu-content" x-bind="popup" x-init="popupEl = $el" x-nq-presence="open" x-anchor.bottom-start.offset.4="$refs.trigger"
        class="z-50 min-w-44 overflow-hidden rounded-floating border border-border bg-popover p-1.5 text-popover-foreground shadow-floating outline-none max-h-[var(--available-height)] overflow-y-auto transition-opacity duration-150 ease-nq data-starting-style:opacity-0 data-ending-style:opacity-0 w-[max(16rem,var(--anchor-width))]"><div data-slot="dropdown-menu-group" role="group" ><div data-slot="dropdown-menu-label" role="presentation" class="px-2.5 pt-1.5 pb-1 text-caption font-medium text-muted-foreground">Workspaces</div>
                            <div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        data-workspace="3x1" x-on:click="ws = $el.dataset.workspace" x-bind:aria-current="ws === $el.dataset.workspace ? &#039;true&#039; : null" class="relative flex min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground h-10"><span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-control">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="3x1" >3</span>
    </span>
                    <span class="min-w-0 flex-1 truncate">3x1</span>
                    <span class="contents" x-show="ws === $el.closest('[data-workspace]').dataset.workspace" ><svg aria-hidden="true" class="text-foreground!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span></div>
                            <div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        data-workspace="personal" x-on:click="ws = $el.dataset.workspace" x-bind:aria-current="ws === $el.dataset.workspace ? &#039;true&#039; : null" class="relative flex min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm text-foreground outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground h-10"><span data-slot="avatar" x-data="nqAvatar(0)"
    class="inline-flex shrink-0 select-none items-center justify-center overflow-hidden bg-secondary align-middle font-medium text-secondary-foreground size-6 text-[10px] rounded-control">
                    <span data-slot="avatar-fallback" class="flex size-full items-center justify-center" x-bind="fallback"
             role="img" aria-label="Fady Mondy" >FM</span>
    </span>
                    <span class="min-w-0 flex-1 truncate">Fady Mondy</span>
                    <span class="contents" x-show="ws === $el.closest('[data-workspace]').dataset.workspace"  style="display: none" ><svg aria-hidden="true" class="text-foreground!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M20 6 9 17l-5-5"/>
</svg></span></div></div>
        <div data-slot="dropdown-menu-separator" role="separator" aria-orientation="horizontal" class="-mx-1.5 my-1.5 h-px bg-border"></div>
                    <div data-slot="dropdown-menu-item" role="menuitem" data-variant="default" x-bind="item"
        data-workspace-create="data-workspace-create" x-on:click="$dispatch(&#039;nq-workspace-create&#039;)" class="relative flex h-nav-row min-h-[var(--nq-touch-min,0px)] cursor-default select-none items-center gap-2.5 rounded-control px-2.5 text-body-sm outline-none data-highlighted:bg-nq-selected data-disabled:pointer-events-none data-disabled:opacity-50 [&_svg]:size-4 [&_svg]:shrink-0 [&_svg]:text-muted-foreground text-muted-foreground"><span class="inline-flex size-6 items-center justify-center rounded-control border border-dashed border-border"><svg class="size-3.5!" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
  <path d="M5 12h14"/>
  <path d="M12 5v14"/>
</svg></span>
                Add workspace</div></div>
</template></div>
</div>
</div>
```
