GodUIGodUI
129Follow on X

Anatomy of the GodUI Menubar

A row of dropdown menus that grow out of their triggers, and what it takes to let one menu exit while the next one enters.

01/03A row of dropdowns
opacity
0 → 1
scale
0.96 → 1
translate
0 -4px → 0 0
tsx
<MenubarPrimitive.Contentdata-slot="menubar-content"align={align}alignOffset={alignOffset}sideOffset={sideOffset}className={cn(  "z-50 min-w-[12rem] origin-(--radix-menubar-content-transform-origin) …   data-[side=bottom]:[--godui-enter-y:-0.25rem] …   data-[state=open]:animate-godui-popover-in   data-[state=closed]:animate-godui-popover-out",  className,)}/>
01

A row of dropdowns

Each menu in the bar is a dropdown menu, and it moves like Dropdown Menu. Radix publishes the point nearest the trigger as --radix-menubar-content-transform-origin, so the godui-popover-in scale grows from there, and the per-side --godui-enter-x/y variables make the menu drift 4px out of the bar.

MenubarSubContent carries the same classes, so a sub-menu grows out of its parent item.

02/03Hopping
tsx
<MenubarPrimitive.Contentdata-slot="menubar-content"ref={composedRef}onInteractOutside={(event) => {  onInteractOutside?.(event);  // Hopping: this menu stays mounted while it animates out, so focus  // landing in (or a press on) the next menu would count as "outside"  // and close the whole menubar. A menu that is already closing never  // dismisses anything.  if (contentRef.current?.dataset.state === "closed") {    event.preventDefault();  }}}…/>
02

Hopping

With one menu open, moving the pointer to the next trigger or pressing an arrow key opens that menu instead. The old menu gets data-state="closed" and plays godui-popover-out (150ms) while the new one plays godui-popover-in (260ms), so the swap reads as one motion.

shadcn's menubar has no exit animation, so its old menu unmounts at once. Keeping it mounted for 150ms exposed a Radix interaction: the new menu takes focus, and the old menu, still listening for focus and presses outside itself, dismisses the whole bar. A closing MenubarContent now ignores outside interactions; only an open menu can dismiss.

03/03Result
03

The result

Open File, then slide the pointer to Edit or press the right arrow key. Hover Share or Find to open a sub-menu.

What's animated

Check and radio indicators pop in from 50% when toggled, not when a menu opens.

InteractionKeyframe / mechanismPropertiesEasingDuration
Open (menu, sub-menu)godui-popover-inopacity, scale 0.96 → 1 from the trigger, translate 4px → 0ease-spring-snappy260ms
Closegodui-popover-outthe same, reversedease-out-expo150ms
Hop to the next menuold menu godui-popover-out, new menu godui-popover-inas above, both at onceas above150ms / 260ms
Check / radio appearsgodui-fade-scale-in from 50%opacity, scaleease-spring-snappy260ms
Trigger and item highlightsnapsnonenonenone

Why GPU-only

Menus, sub-menus and indicators animate opacity, scale and translate. Trigger and item highlights are background changes, so GodUI lets them snap instead of animating paint.

Reduced motion

--godui-motion: 0 removes the scale and drift from both keyframes. Menus and their indicators fade, in 150ms, and a hop is a crossfade.

Replacing shadcn

npx shadcn add @godui/menubar overwrites components/ui/menubar.tsx. Exports, props and data-slot attributes match shadcn/ui new-york-v4, so existing imports keep working. It also installs godui-motion (easings, keyframes, the FLIP hook) into your project.

shadcn's MenubarContent has no exit animation, so a menu vanishes the moment you leave it. GodUI adds one, which means the old menu is still mounted while the next one opens. MenubarContent therefore ignores outside focus and presses while it is closing (data-state="closed"); without that, the exiting menu would treat the next menu as "outside" and close the whole menubar. An open menu dismisses as before, and your own onInteractOutside, onFocusOutside and onPointerDownOutside still run. MenubarSubContent does the same while a sub-menu animates out.