Source: https://monarchui.dev/components/space-switcher/

Components · Identity

# SpaceSwitcher

Pick "All spaces" or one Space of the workspace, with search, New and Manage links.

@rookcom/monarch-react/shell

Component

## All spaces, with New and Manage

## Usage examples

### Collapsed, and without All spaces

## Purpose

Rook workspaces are split into Spaces (department-like slices managed in Rook ID). The switcher sets which Space the app shows. Its listbox supports arrow-key navigation.

### Use when

- An app that scopes its content by Space, in the rail’s `spaceSwitcher` slot or a page header.

### Avoid when

- Switching workspaces: use WorkspaceSwitcher.
- Filtering a single list by team or department: use a filter in FilterBar.

## Design principles

- Data in, choice out: the app passes the Spaces this person may open and stores the choice; the switcher never fetches or persists.
- One listbox model (EntityPicker): arrow keys, Home/End, type to search, Enter to choose, Escape to close; options are real `option`s, not buttons.
- "All spaces" is a first-class choice, shown first and checked when nothing is chosen.
- A Space that disappears (deleted, access removed) reads as "All spaces" instead of a stale name.

## API reference

```tsx
import { SpaceSwitcher } from '@rookcom/monarch-react/shell';
```

| Export | Kind | Rendering | Root class |
| --- | --- | --- | --- |
| `SpaceSwitcher` | component | client | `.mn-space-switcher` |

Forwards its ref to the trigger button. Built on EntityPicker from `@rookcom/monarch-react`.

`SpaceOption` is `{ id, name, description?, icon? }`.

Generated from the TypeScript declarations of `packages/react/src/components/identity/space-switcher`.

## Properties

`value` is a Space id or `null` for "All spaces"; controlled with `value` + `onValueChange`, or uncontrolled with `defaultValue`.

`manageHref` opens in a new tab unless `manageExternal={false}`; `onCreate` or `createHref` adds "New Space".

### `SpaceSwitcher`

Chooses the Space (a department-like slice of a workspace) the app shows: "All spaces" or one Space. A popover with search, the current Space checked, and New / Manage links; the list is a listbox with arrow keys, Home/End and type-to-search (EntityPicker). The product supplies the Spaces and stores the choice; the switcher never fetches.

| Prop | Type | Description |
| --- | --- | --- |
| `spaces` required | `SpaceOption[]` | The Spaces of the current workspace that this person can open. |
| `align` | `"center" \| "start" \| "end"` | Popover alignment. Default `start`. |
| `allowAll` | `boolean` | Offer "All spaces" first. Default true. Without it a Space must stay chosen. |
| `className` | `string` |  |
| `collapsed` | `boolean` | Icon-only trigger for a collapsed rail; the name moves into a tooltip. |
| `createHref` | `string` | "New Space": a link… |
| `defaultValue` | `string \| null` |  |
| `hideWhenEmpty` | `boolean` | Render nothing when there are no Spaces and nothing is loading. Default true. |
| `loading` | `boolean` | Spaces are still loading: the list says so. |
| `manageExternal` | `boolean` | Open `manageHref` in a new tab. Default true. |
| `manageHref` | `string` | "Manage Spaces" link (usually Rook ID). |
| `messages` | `Partial<SpaceSwitcherMessages>` | Interface text, for translation. |
| `onCreate` | `(() => void)` | …or an action (the popover closes first). |
| `onOpenChange` | `((open: boolean) => void)` |  |
| `onValueChange` | `((spaceId: string \| null) => void)` |  |
| `open` | `boolean` | Open (controlled). |
| `value` | `string \| null` | The chosen Space id; `null` is "All spaces" (controlled). |

## Variants

| Prop | Values | Description |
| --- | --- | --- |
| `collapsed` | `'false'``'true'` | Icon-only trigger with the current Space in a tooltip, for the 48px rail. |
| `allowAll` | `'true'``'false'` | Offer "All spaces". Without it a Space must stay chosen. |

## States

| State | Exposed as | Behaviour |
| --- | --- | --- |
| All spaces | `data-all` | Nothing chosen: the trigger reads "Space: All spaces". |
| Current Space | `aria-selected="true"` | Checked in the list and named on the trigger. |
| Loading | – | The list says "Loading spaces…" while `loading`. |
| Empty | – | With no Spaces and nothing loading the switcher renders nothing (`hideWhenEmpty`), as the products did. |
| Open | `aria-expanded="true"` | On the trigger. |

## Interaction behaviour

- Clicking the trigger (or Enter, Space, ↓) opens the popover with focus in the search box.
- Typing filters by name; choosing closes the popover and returns focus to the trigger.
- "New Space" closes the popover, then runs `onCreate` (or follows `createHref`); "Manage Spaces" opens Rook ID in a new tab.

## Keyboard support

| Keys | Action |
| --- | --- |
| Enter / Space / ↓ | Open the popover from the trigger. |
| ↓ / ↑ | Move through the Spaces. |
| Home / End | First / last Space. |
| Enter | Choose the highlighted Space. |
| Tab | From the search box to New / Manage. |
| Escape | Close and return focus to the trigger. |

## Accessibility

Role

button opening a dialog that contains a combobox (search) and a listbox

Accessible name

The trigger is "Space: <current>" (

messages.label

); the popover is named "Space".

- Follows the APG combobox pattern with a listbox popup (EntityPicker); the active option is tracked with `aria-activedescendant`.
- The collapsed trigger keeps its full name and adds a tooltip.
- The Manage link announces that it opens in a new tab.

## Responsive behaviour

- The trigger fills its container and truncates long names.
- The popover is 18rem wide, never wider than the screen minus 2rem; New / Manage rows are 44px tall on touch screens.

## Theme support

- Surface, border and text tokens; inside RookRail the trigger switches to the accent panel colours.
- Transitions are removed under reduced motion.

## Do and don't

### Do

- Pass only the Spaces this person may open, and still check access on the server.
- Remember the choice per workspace in the app (Rook apps share it across apps).
- Use the rail slot function to pass `collapsed`.

### Don't

- Do not fetch Spaces inside a slot render on every render; load them once and pass the list.
- Do not use it for navigation between pages.

## Related components

- [WorkspaceSwitcher](https://monarchui.dev/components/workspace-switcher/): The workspace above the Space.
- [EntityPicker](https://monarchui.dev/components/entity-picker/): The picker it is built on.
- [RookRail](https://monarchui.dev/components/rook-rail/): Has a slot for it.

## Dependencies

| Kind | Depends on |
| --- | --- |
| npm packages | `react` |
| Monarch components | [EntityPicker](https://monarchui.dev/components/entity-picker/) |

## Testing coverage

- Trigger named with the current Space; starts on All spaces.
- Popover with search focused, the current Space selected and the Manage link.
- Typing filters; arrow keys and Enter choose; focus returns to the trigger.
- All spaces clears the choice; `allowAll={false}` hides it; controlled mode; a missing Space reads as All spaces.
- New Space runs after closing; Escape returns focus; hidden when empty, loading message; collapsed tooltip; translation.

| Check | Covered |
| --- | --- |
| Axe in jsdom | Yes |
| Server rendering | Yes |
| Browser suites | Visual baselines: /__gallery/space-switcher; Cross-browser axe: /components/space-switcher |

Tests: `packages/react/src/components/identity/space-switcher`

## All example code

### All spaces, with New and Manage

```tsx
import { useState } from 'react';
import { SpaceSwitcher } from '@rookcom/monarch-react/shell';

const spaces = [
  { id: 's-eng', name: 'Engineering', description: '18 members' },
  { id: 's-mkt', name: 'Marketing', description: '9 members' },
  { id: 's-ops', name: 'Operations', description: '6 members' },
  { id: 's-sales', name: 'Sales', description: '12 members' },
];

function Basic() {
  const [space, setSpace] = useState<string | null>(null);
  return (
    <div style={{ width: '15rem' }}>
      <SpaceSwitcher
        spaces={spaces}
        value={space}
        onValueChange={setSpace}
        manageHref="https://id.example.com/spaces"
        onCreate={() => {}}
      />
    </div>
  );
}

<Basic />
```

### Collapsed, and without All spaces

```tsx
import { SpaceSwitcher } from '@rookcom/monarch-react/shell';

const spaces = [
  { id: 's-eng', name: 'Engineering', description: '18 members' },
  { id: 's-mkt', name: 'Marketing', description: '9 members' },
  { id: 's-ops', name: 'Operations', description: '6 members' },
  { id: 's-sales', name: 'Sales', description: '12 members' },
];

function Compact() {
  return (
    <div style={{ display: 'flex', gap: 'var(--mn-space-3)', alignItems: 'center' }}>
      <SpaceSwitcher spaces={spaces} defaultValue="s-mkt" collapsed />
      <div style={{ width: '13rem' }}>
        <SpaceSwitcher spaces={spaces} defaultValue="s-ops" allowAll={false} />
      </div>
    </div>
  );
}

<Compact />
```
