diff --git a/packages/editor/README.md b/packages/editor/README.md
index bfbc95ac07..885afd2ccd 100644
--- a/packages/editor/README.md
+++ b/packages/editor/README.md
@@ -39,7 +39,7 @@ export function ProjectPage({ projectId }: { projectId: string }) {
| Prop | What it does |
| --- | --- |
| `loadingSlot` | Your loading surface, shown in the canvas panes of both layouts and in 2D and 3D while the scene loads. The editor still decides when loading is over and fires `onLoaderChange`; a failed load always shows the editor's error and retry screen. Omit it or pass `null` for the default loader. |
-| `actionMenuPlacement` | `'bottom'` (default) or `'top'`. With `'top'`, the v2 layout gives the tool menu its own row under the toolbar, centred on the scene panes and following them in split, so the bottom strip is free; v1 floats it at the top edge. Mobile keeps the bottom rail. |
+| `actionMenuPlacement` | `'bottom'` (default), `'top'`, `'left'` or `'right'`. With `'top'`, the v2 layout gives the tool menu its own row under the toolbar, centred on the scene panes and following them in split, so the bottom strip is free; v1 floats it at the top edge. `'left'` and `'right'` dock it as a vertical rail. This is only the default: users can move the menu from its own dock picker, and their pick is kept per browser. Mobile keeps the bottom rail. |
| `viewBar` | Each pane's view bar (v2, default `true`). Turn it off for a host that shows one fixed view. |
| `sceneOverlay` | Chrome over the 3D and 2D panes with the level selector and tool dock, hidden while no scene view is on screen. |
| `stageOverlay` | A full-bleed surface over the whole stage; the scene stays mounted underneath. To show something beside the scene, register a view instead. |
diff --git a/packages/editor/src/components/editor/index.tsx b/packages/editor/src/components/editor/index.tsx
index 6664b51bab..8006731182 100644
--- a/packages/editor/src/components/editor/index.tsx
+++ b/packages/editor/src/components/editor/index.tsx
@@ -128,7 +128,7 @@ import { SnapshotCaptureOverlay } from './snapshot-capture-overlay'
import { type SnapshotCameraData, ThumbnailGenerator } from './thumbnail-generator'
import { VectorEdgeExtractor } from './vector-edge-extractor'
import { HostView } from './views/host-view'
-import { SceneRegion } from './views/scene-region'
+import { SceneRegion, useSceneSpan } from './views/scene-region'
import { useActiveViewLayout, useEditorViews } from './views/use-editor-views'
import { WallMeasurementLabel } from './wall-measurement-label'
import { WallMoveSideHandles } from './wall-move-side-handles'
@@ -1446,8 +1446,14 @@ function EditorContent({
commandPaletteEmptyAction,
}: EditorProps) {
const isMobile = useIsMobile()
+ // The user's own pick wins over the host's default placement.
+ const userMenuPlacement = useEditor((state) => state.actionMenuPlacement)
+ const menuPlacement = userMenuPlacement ?? actionMenuPlacement ?? 'bottom'
// A top-docked menu sits at the top of the scene panes on desktop (layout v2).
- const dockMenuOnTop = actionMenuPlacement === 'top' && !isMobile
+ const dockMenuOnTop = menuPlacement === 'top' && !isMobile
+ const dockMenuOnRight = menuPlacement === 'right' && !isMobile
+ const dockMenuOnLeft = menuPlacement === 'left' && !isMobile
+ const reserveBottomMenu = menuPlacement === 'bottom' || isMobile
const isFirstPersonMode = useEditor((s) => s.isFirstPersonMode)
const isStudioMode = useEditor((s) => s.workspaceMode === 'studio')
const presentationProjectId = projectId ?? null
@@ -1504,10 +1510,14 @@ function EditorContent({
const isCaptureMode = useEditor((s) => s.isCaptureMode)
const readOnly = useScene((s) => s.readOnly)
// Chrome only steps down for the top menu while the menu is actually there.
- const topMenuShown =
- dockMenuOnTop &&
- !(isVersionPreviewMode || isCaptureMode || isStudioMode) &&
- shouldShowEditingControls(readOnly)
+ const editingControlsShown = shouldShowEditingControls(readOnly)
+ // Layout v2 also hides the menu in version preview, capture and studio.
+ const menuShown = editingControlsShown && !(isVersionPreviewMode || isCaptureMode || isStudioMode)
+ const topMenuShown = dockMenuOnTop && menuShown
+ const leftMenuShown = dockMenuOnLeft && menuShown
+ const hasSceneView = useSceneSpan() !== null
+ // Every dock is scene chrome: it leaves with the last 3D or 2D pane.
+ const rightMenuShown = dockMenuOnRight && menuShown && hasSceneView
const sidebarWidth = useSidebarStore((s) => s.width)
const isSidebarCollapsed = useSidebarStore((s) => s.isCollapsed)
@@ -1830,8 +1840,11 @@ function EditorContent({
overlays={
<>