SceneMenuBar conventions
PostHog scenes render a Mac-style menu bar above <SceneTitleSection> that consolidates
ScenePanel actions into a discoverable, keyboard-navigable surface. This skill documents
the taxonomy, the available primitives, and the wiring rules.
The whole bar is gated by the SCENE_MENU_BAR feature flag (see lib/constants.tsx).
Components
All primitives live in frontend/src/layout/scenes/components/SceneMenuBar.tsx.
| Component |
Use for |
<SceneMenuBar> |
Top-level wrapper; renders above <SceneTitleSection>. Includes the universal right cluster (PostHog AI / Docs / Support). |
<SceneMenuBarMenu label> |
Top-level menu (File / Edit / View / Metadata / Staff only). |
<SceneMenuBarSubMenu label> |
Nested sub-menu inside a SceneMenuBarMenu (e.g. Create, Export, Add to notebook). Auto-prepends a blank icon slot to the trigger so the label aligns with icon-bearing siblings — pass withIconBlank={false} to opt out. |
<SceneMenuBarItem> |
Standard menu item (action / navigation). |
<SceneMenuBarCheckboxItem> |
Toggle item — reflects boolean state via checkmark. |
<SceneMenuBarRadioGroup> + <SceneMenuBarRadioItem> |
One-of-many mutually-exclusive options. |
<SceneMenuBarSeparator> |
Horizontal divider between groups. |
<SceneMenuBarShortcut> |
Right-aligned keyboard shortcut hint. |
<SceneMenuBarPopover label> |
Drop-in alternative to SceneMenuBarMenu when content needs rich form controls (text inputs, comboboxes). Uses Popover under the hood — Menu.Popup intercepts keystrokes and blocks inputs. Trigger does NOT participate in CompositeRoot keyboard nav. |
Canonical menu set (in order)
| Menu |
Items |
| File |
+ Create sub-menu (top), ── , project tree items (Open / Move / Star), Copy to another project, Manage with Terraform, scene-specific file ops, Export sub-menu, ──, Delete / Archive / Restore (destructive at the very bottom) |
| Edit |
Duplicate, Rename, Edit in SQL editor, scene-specific edits, state mutations (Pause/Resume, Activate/Deactivate), ──, toggle group (checkbox/radio items, separated by <SceneMenuBarSeparator> from regular items) |
| View (conditional) |
Cross-resource viewing — View recordings, View metalytics, View related insights, etc. Anything that navigates to see something tangentially related to the current resource. Skip when empty. |
Metadata (use SceneMenuBarPopover) |
Tags, Evaluation contexts, Stage, Activity indicator, External references, Comments, etc. |
| Staff only (conditional) |
Debug panels, staff-only toggles. Only render for staff / superpowers / impersonated / non-cloud users. |
State lives in Edit. Pause/Resume, Activate/Deactivate, Favorite/Pin toggles and similar
state mutations all live inside the Edit menu — usually at the bottom, after a
<SceneMenuBarSeparator> if there's a toggle group. Don't create a separate State menu.
Right cluster is fixed and universal: PostHog AI · Docs · Support. Do not add to it.
Rules
Destructive actions (Delete / Archive / Restore)
- Live at the bottom of File, separated from the file ops above by a
<SceneMenuBarSeparator>.
- Pass
variant="destructive" for visual treatment (red text + icon).
- Do not add
opensFloatingUi to destructive items even when they open a confirmation dialog — the destructive variant + clear label are signal enough.
- Wrap with
<AccessControlAction> where the resource has access control levels.
Toggle groups
- Group all
<SceneMenuBarCheckboxItem> and <SceneMenuBarRadioGroup> items together.
- Separate them from regular action items with
<SceneMenuBarSeparator> above and (if more items follow) below.
- Use stable labels — the checkmark conveys state:
Pinned not Pin/Unpin, Show debug panel not Show/Hide debug panel.
- Radios for mutually-exclusive choices (
<SceneMenuBarRadioGroup value onValueChange>).
opensFloatingUi prop
Append … to the item label as a Mac-style affordance for items that open additional floating UI:
- ✅ Use for: modal, popover, dialog, side panel openers
- ❌ Skip for: same-page navigations, direct actions, destructive items (the destructive style already signals consequence), toggle items
+ Create cross-sells
- Nest inside a
<SceneMenuBarSubMenu label="Create"> at the top of File.
- Place a
<SceneMenuBarSeparator> between Create and the rest of File.
- Cross-sells: Cohort, Survey, Dashboard, Notebook, Endpoint, Subscription, Alert, Share or embed.
Empty menus
SceneMenuBarMenu auto-disables its trigger when its children render nothing at compile
time — e.g. {false && <Item/>}, {null}, or an empty fragment. The trigger stays
visible (greyed out, cursor-not-allowed) so the menu set still communicates the bar's
capabilities to the user.
For menus whose children may render null at runtime (the most common case is
<SceneMenuBarFileItems>, which returns null when no project-tree entry is registered),
the parent cannot detect that the popup will be empty. You must either:
Pass disabled explicitly based on the same value the children depend on, e.g.:
const { projectTreeRefEntry } = useValues(projectTreeDataLogic)
const hasFileItems = !!projectTreeRefEntry
<SceneMenuBarMenu label="File" disabled={!hasFileItems}>
{hasFileItems && <SceneMenuBarFileItems dataAttrKey="…" />}
</SceneMenuBarMenu>
Gate the whole menu at the parent if it can't render anything useful:
{
hasAnyEditItem && <SceneMenuBarMenu label="Edit">…</SceneMenuBarMenu>
}
Prefer (1) when the menu's presence is part of the scene's identity (File should always
be visible even if disabled), and (2) when the menu is genuinely optional (View, Staff
only).
Tags / Evaluation contexts / other rich inputs
- Must live inside a
<SceneMenuBarPopover>, not a <SceneMenuBarMenu>. Menu popups
intercept keystrokes (typeahead, arrow nav) and prevent text inputs from receiving
input.
- Use
<TagsCombobox> (lib/components/Scenes/TagsCombobox.tsx) for multi-select chip
inputs — selection-only by default; pass allowCustomValues to surface a
"Create new {noun} '...'" item at the bottom.
- Use
<SceneTagsCombobox> (lib/components/Scenes/SceneTagsCombobox.tsx) as the
ready-made wrapper that swaps in for <SceneTags> under SCENE_MENU_BAR.
Optimistic saves with debounce
When autosaving from menubar inputs (tags, toggles), use a kea listener with:
- Optimistic update (
setX + updateX) immediately, before the API call.
await breakpoint(250) to debounce rapid changes.
breakpoint() after the API response to bail if a newer call has superseded this one.
- Set-equality check before reconciling with server response to avoid re-ordering
when the server returns differently-ordered values.
- Re-throw
error.isBreakpoint so kea swallows it silently.
See saveTagsInline in frontend/src/scenes/feature-flags/featureFlagLogic.ts.
File menu — project tree items
Use <SceneMenuBarFileItems dataAttrKey={RESOURCE_TYPE} /> at the bottom of File
(before the destructive separator) to render Open in project tree, Move to folder,
Add/Remove starred. It auto-hides when no project-tree entry is registered.
Right cluster
Lives outside the <Menubar> wrapper to keep CompositeRoot keyboard nav working for
the actual menus. Do not add items here without UX review.
Layout-aware bleed
<SceneMenuBar> reads sceneLayoutLogic.sceneLayoutConfig.layout and bleeds past the
scene container's padding (-mx-4 -mt-4) only when the layout is padded:
app, app-container, or app-full-scene-height. For unpadded layouts (app-raw,
app-raw-no-header, plain) the negatives would overshoot and visually break the
header, so they're skipped. You don't need to do anything in your scene — just register
the right SceneConfig.layout and the bar adapts. Sentinel: a data-scene-layout
attribute is set on the wrapper for debugging.
Scenes already migrated
- Feature Flag —
frontend/src/scenes/feature-flags/FeatureFlag.tsx
- Insight —
frontend/src/scenes/insights/SidePanel/InsightSceneMenuBar.tsx
- Dashboard —
frontend/src/scenes/dashboard/DashboardSceneMenuBar.tsx
When migrating a new scene, follow the <Scene>SceneMenuBar.tsx pattern (component
that reads featureFlagLogic, early-returns null if the flag is off, otherwise renders
the bar). Mount it directly above <SceneTitleSection>.
Auditing a scene
When migrating a scene, take its existing ScenePanel and map every item to:
- Which menu it belongs to (File / Edit / View / Metadata / Staff only).
- Whether it needs
variant="destructive" (delete/archive/remove).
- Whether it needs
opensFloatingUi (opens modal/popover/dialog/side panel).
- Whether it's a toggle (use
SceneMenuBarCheckboxItem).
- What gates apply (saved state, FF, AccessControl, multi-project).
See /Users/adamleithp/Desktop/scene-menu-bar-grouping.md for the running inventory of
all ScenePanel items across PostHog and their proposed menu placement.
1---
2name: scene-menu-bar
3description: Conventions for adding or editing the SceneMenuBar above a scene's <SceneTitleSection>. Use when adding new menu items, moving items between menus, gating behind feature flags, building a new scene's menubar, or wiring rich inputs (tags, combobox) into the bar.
4---
5
6# SceneMenuBar conventions
7
8PostHog scenes render a Mac-style menu bar above `<SceneTitleSection>` that consolidates
9ScenePanel actions into a discoverable, keyboard-navigable surface. This skill documents
10the taxonomy, the available primitives, and the wiring rules.
11
12The whole bar is gated by the `SCENE_MENU_BAR` feature flag (see `lib/constants.tsx`).
13
14## Components
15
16All primitives live in `frontend/src/layout/scenes/components/SceneMenuBar.tsx`.
17
18| Component | Use for |
19| ------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
20| `<SceneMenuBar>` | Top-level wrapper; renders above `<SceneTitleSection>`. Includes the universal right cluster (PostHog AI / Docs / Support). |
21| `<SceneMenuBarMenu label>` | Top-level menu (File / Edit / View / Metadata / Staff only). |
22| `<SceneMenuBarSubMenu label>` | Nested sub-menu inside a `SceneMenuBarMenu` (e.g. `Create`, `Export`, `Add to notebook`). Auto-prepends a blank icon slot to the trigger so the label aligns with icon-bearing siblings — pass `withIconBlank={false}` to opt out. |
23| `<SceneMenuBarItem>` | Standard menu item (action / navigation). |
24| `<SceneMenuBarCheckboxItem>` | Toggle item — reflects boolean state via checkmark. |
25| `<SceneMenuBarRadioGroup>` + `<SceneMenuBarRadioItem>` | One-of-many mutually-exclusive options. |
26| `<SceneMenuBarSeparator>` | Horizontal divider between groups. |
27| `<SceneMenuBarShortcut>` | Right-aligned keyboard shortcut hint. |
28| `<SceneMenuBarPopover label>` | Drop-in alternative to `SceneMenuBarMenu` when content needs rich form controls (text inputs, comboboxes). Uses Popover under the hood — Menu.Popup intercepts keystrokes and blocks inputs. Trigger does NOT participate in CompositeRoot keyboard nav. |
29
30## Canonical menu set (in order)
31
32| Menu | Items |
33| ------------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
34| **File** | `+ Create` sub-menu (top), ── , project tree items (Open / Move / Star), Copy to another project, Manage with Terraform, scene-specific file ops, Export sub-menu, ──, **Delete / Archive / Restore** (destructive at the very bottom) |
35| **Edit** | Duplicate, Rename, Edit in SQL editor, scene-specific edits, state mutations (Pause/Resume, Activate/Deactivate), ──, toggle group (checkbox/radio items, separated by `<SceneMenuBarSeparator>` from regular items) |
36| **View** _(conditional)_ | Cross-resource viewing — View recordings, View metalytics, View related insights, etc. Anything that navigates to _see_ something tangentially related to the current resource. Skip when empty. |
37| **Metadata** _(use `SceneMenuBarPopover`)_ | Tags, Evaluation contexts, Stage, Activity indicator, External references, Comments, etc. |
38| **Staff only** _(conditional)_ | Debug panels, staff-only toggles. Only render for staff / superpowers / impersonated / non-cloud users. |
39
40> **State lives in Edit.** Pause/Resume, Activate/Deactivate, Favorite/Pin toggles and similar
41> state mutations all live inside the Edit menu — usually at the bottom, after a
42> `<SceneMenuBarSeparator>` if there's a toggle group. Don't create a separate `State` menu.
43
44Right cluster is fixed and universal: **PostHog AI · Docs · Support**. Do not add to it.
45
46## Rules
47
48### Destructive actions (Delete / Archive / Restore)
49
50- Live at the **bottom of File**, separated from the file ops above by a `<SceneMenuBarSeparator>`.
51- Pass `variant="destructive"` for visual treatment (red text + icon).
52- Do **not** add `opensFloatingUi` to destructive items even when they open a confirmation dialog — the destructive variant + clear label are signal enough.
53- Wrap with `<AccessControlAction>` where the resource has access control levels.
54
55### Toggle groups
56
57- Group all `<SceneMenuBarCheckboxItem>` and `<SceneMenuBarRadioGroup>` items together.
58- Separate them from regular action items with `<SceneMenuBarSeparator>` above and (if more items follow) below.
59- Use stable labels — the checkmark conveys state: `Pinned` not `Pin/Unpin`, `Show debug panel` not `Show/Hide debug panel`.
60- Radios for mutually-exclusive choices (`<SceneMenuBarRadioGroup value onValueChange>`).
61
62### `opensFloatingUi` prop
63
64Append `…` to the item label as a Mac-style affordance for items that open additional floating UI:
65
66- ✅ **Use** for: modal, popover, dialog, side panel openers
67- ❌ **Skip** for: same-page navigations, direct actions, destructive items (the destructive style already signals consequence), toggle items
68
69### `+ Create` cross-sells
70
71- Nest inside a `<SceneMenuBarSubMenu label="Create">` at the **top of File**.
72- Place a `<SceneMenuBarSeparator>` between Create and the rest of File.
73- Cross-sells: Cohort, Survey, Dashboard, Notebook, Endpoint, Subscription, Alert, Share or embed.
74
75### Empty menus
76
77`SceneMenuBarMenu` auto-disables its trigger when its children render nothing at compile
78time — e.g. `{false && <Item/>}`, `{null}`, or an empty fragment. The trigger stays
79visible (greyed out, `cursor-not-allowed`) so the menu set still communicates the bar's
80capabilities to the user.
81
82For menus whose children may render null at runtime (the most common case is
83`<SceneMenuBarFileItems>`, which returns null when no project-tree entry is registered),
84the parent **cannot detect** that the popup will be empty. You must either:
85
861. **Pass `disabled` explicitly** based on the same value the children depend on, e.g.:
87
88 ```tsx
89 const { projectTreeRefEntry } = useValues(projectTreeDataLogic)
90 const hasFileItems = !!projectTreeRefEntry
91 <SceneMenuBarMenu label="File" disabled={!hasFileItems}>
92 {hasFileItems && <SceneMenuBarFileItems dataAttrKey="…" />}
93 </SceneMenuBarMenu>
94 ```
95
962. **Gate the whole menu** at the parent if it can't render anything useful:
97
98 ```tsx
99 {
100 hasAnyEditItem && <SceneMenuBarMenu label="Edit">…</SceneMenuBarMenu>
101 }
102 ```
103
104Prefer (1) when the menu's presence is part of the scene's identity (File should always
105be visible even if disabled), and (2) when the menu is genuinely optional (View, Staff
106only).
107
108### Tags / Evaluation contexts / other rich inputs
109
110- Must live inside a `<SceneMenuBarPopover>`, not a `<SceneMenuBarMenu>`. Menu popups
111 intercept keystrokes (typeahead, arrow nav) and prevent text inputs from receiving
112 input.
113- Use `<TagsCombobox>` (`lib/components/Scenes/TagsCombobox.tsx`) for multi-select chip
114 inputs — selection-only by default; pass `allowCustomValues` to surface a
115 "Create new {noun} '...'" item at the bottom.
116- Use `<SceneTagsCombobox>` (`lib/components/Scenes/SceneTagsCombobox.tsx`) as the
117 ready-made wrapper that swaps in for `<SceneTags>` under `SCENE_MENU_BAR`.
118
119### Optimistic saves with debounce
120
121When autosaving from menubar inputs (tags, toggles), use a kea listener with:
122
1231. **Optimistic update** (`setX` + `updateX`) immediately, before the API call.
1242. **`await breakpoint(250)`** to debounce rapid changes.
1253. **`breakpoint()` after the API response** to bail if a newer call has superseded this one.
1264. **Set-equality check** before reconciling with server response to avoid re-ordering
127 when the server returns differently-ordered values.
1285. Re-throw `error.isBreakpoint` so kea swallows it silently.
129
130See `saveTagsInline` in `frontend/src/scenes/feature-flags/featureFlagLogic.ts`.
131
132### File menu — project tree items
133
134Use `<SceneMenuBarFileItems dataAttrKey={RESOURCE_TYPE} />` at the bottom of File
135(before the destructive separator) to render Open in project tree, Move to folder,
136Add/Remove starred. It auto-hides when no project-tree entry is registered.
137
138### Right cluster
139
140Lives outside the `<Menubar>` wrapper to keep CompositeRoot keyboard nav working for
141the actual menus. Do not add items here without UX review.
142
143### Layout-aware bleed
144
145`<SceneMenuBar>` reads `sceneLayoutLogic.sceneLayoutConfig.layout` and bleeds past the
146scene container's padding (`-mx-4 -mt-4`) **only** when the layout is padded:
147`app`, `app-container`, or `app-full-scene-height`. For unpadded layouts (`app-raw`,
148`app-raw-no-header`, `plain`) the negatives would overshoot and visually break the
149header, so they're skipped. You don't need to do anything in your scene — just register
150the right `SceneConfig.layout` and the bar adapts. Sentinel: a `data-scene-layout`
151attribute is set on the wrapper for debugging.
152
153## Scenes already migrated
154
155- Feature Flag — `frontend/src/scenes/feature-flags/FeatureFlag.tsx`
156- Insight — `frontend/src/scenes/insights/SidePanel/InsightSceneMenuBar.tsx`
157- Dashboard — `frontend/src/scenes/dashboard/DashboardSceneMenuBar.tsx`
158
159When migrating a new scene, follow the `<Scene>SceneMenuBar.tsx` pattern (component
160that reads `featureFlagLogic`, early-returns null if the flag is off, otherwise renders
161the bar). Mount it directly above `<SceneTitleSection>`.
162
163## Auditing a scene
164
165When migrating a scene, take its existing ScenePanel and map every item to:
166
1671. Which menu it belongs to (File / Edit / View / Metadata / Staff only).
1682. Whether it needs `variant="destructive"` (delete/archive/remove).
1693. Whether it needs `opensFloatingUi` (opens modal/popover/dialog/side panel).
1704. Whether it's a toggle (use `SceneMenuBarCheckboxItem`).
1715. What gates apply (saved state, FF, AccessControl, multi-project).
172
173See `/Users/adamleithp/Desktop/scene-menu-bar-grouping.md` for the running inventory of
174all ScenePanel items across PostHog and their proposed menu placement.