Workspaces
Workspaces are virtual desktops. Three ship out of the box; each open window is tagged with the workspace it was opened on. Switching workspaces only filters the window layer; every window keeps its bounds and internal state, so it’s there waiting when you come back.
The menu bar shows a row of pips next to the clock (one per workspace, the active one is wider and accent-colored). Click any pip to jump to that workspace; the keyboard chord moves at the speed of muscle memory.
Keyboard
Section titled “Keyboard”| Shortcut | Effect |
|---|---|
Ctrl + Alt + ← | Previous workspace (wraps) |
Ctrl + Alt + → | Next workspace (wraps) |
Ctrl + Alt + Shift + ←/→ | Same, and bring the focused window along |
| Right-click a window’s title bar | Pick “Move to Workspace N” from the menu |
Every binding bails when the event target is an <input>, <textarea>, or contenteditable element. Workspaces never hijack typing.
Behavioral notes
Section titled “Behavioral notes”- Focus follows you. Switching to a workspace restores the focus to whichever window was on top there. Returning to an empty workspace drops focus.
- Opening across workspaces. Calling
openWindow({ kind: "app", appId })from anywhere, when the window already exists on another workspace, pulls you to that workspace and focuses the window. Mirrors what Cmd+Tab does on macOS. - Closing the focused window hands focus to the next-highest-z window on the current workspace, not across spaces.
appearsAsDesktopIconpredicates are evaluated against the storage adapter, not per-workspace. Desktop icons appear on every workspace consistently; this is the same model as macOS: folders and shortcuts live “on the desktop,” not “on Space 2.”
Using the API
Section titled “Using the API”useWindowManager() now exposes four workspace actions:
const { state, // includes workspaces[], activeWorkspaceId switchWorkspace, // (workspaceId) => void moveWindowToWorkspace, // (windowId, workspaceId) => void addWorkspace, // (workspaceId?) => void, id auto-generated when omitted removeWorkspace, // (workspaceId) => void, migrates windows to the first remaining} = useWindowManager();removeWorkspace refuses to drop the last workspace (a desktop needs at least one). It migrates windows on the removed workspace to the first remaining workspace rather than silently deleting them.
Custom workspace counts
Section titled “Custom workspace counts”Replace the seed list by dispatching ADD_WORKSPACE and REMOVE_WORKSPACE from your app boot, or render your own menu-bar widget that calls addWorkspace() on click:
import { useWindowManager } from "@react-ui-os/core";
function AddWorkspaceButton() { const { state, addWorkspace } = useWindowManager(); return ( <button type="button" onClick={() => addWorkspace()} disabled={state.workspaces.length >= 9} > + Workspace </button> );}(The library doesn’t ship this as a default: three felt like the right initial count, and many apps deliberately reduce to one to hide the feature.)
Migration note
Section titled “Migration note”Adding the workspace field is a non-breaking change at the call-site level (every existing useWindowManager action still works) but the OpenWindow shape now includes a required workspaceId: string. Consumers persisting their own WM snapshots to disk will see the field on deserialization; treat missing values as the active workspace at load time.
Accessibility
Section titled “Accessibility”- The pip cluster in the menu bar is
role="tablist" aria-label="Workspaces"; each pip is a<button role="tab">with anaria-selectedstate. - The HUD shows the workspace number on switch so screen readers (with
aria-live="polite") announce the change. - Keyboard shortcuts are independent of pointer interaction.
See also
Section titled “See also”- App switcher: Cmd+Tab follows windows across workspaces.
- Mission Control: currently shows windows on the active workspace only.
useWindowManager: forstate.workspacesandstate.activeWorkspaceId.