GodUIGodUI
129Follow on X

Anatomy of the GodUI Dropdown Menu

A menu that grows out of its trigger, sub-menus that grow out of their item, and a check mark that pops: three uses of two shared keyframes.

01/03From the trigger
opacity
0 → 1
scale
0.96 → 1
translate
0 -4px → 0 0
tsx
<DropdownMenuPrimitive.Contentdata-slot="dropdown-menu-content"className={cn(  "z-50 … origin-(--radix-dropdown-menu-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

From the trigger

The menu uses the same godui-popover-in keyframe as Popover. Radix publishes the point nearest the trigger as --radix-dropdown-menu-content-transform-origin, so the scale grows from there, and the per-side --godui-enter-x/y variables make it drift 4px out of the trigger.

DropdownMenuSubContent carries the same classes. A sub-menu opening to the right has data-side="right", so it grows out of its parent item, leftmost edge first.

02/03The check pop
scale
0.5 → 1
opacity
0 → 1
tsx
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center"><DropdownMenuPrimitive.ItemIndicator className="[--godui-enter-scale:0.5]  group-data-[animate=true]/dropdown-menu-checkbox-item:data-[state=checked]:animate-godui-fade-scale-in">  <CheckIcon className="size-4" /></DropdownMenuPrimitive.ItemIndicator></span>
02

The check pop

Radix mounts ItemIndicator only while its item is checked, so a keyframe on the indicator plays when the check appears. But the menu's items mount every time it opens, and an already-checked item would pop on each open. So the keyframe is gated on the item's data-animate, which the item sets only once its checked value changes: opening the menu shows the checks still, and a toggle while it's open pops. The keyframe also requires the indicator's own data-state="checked". Radix's Presence keeps an unchecked indicator mounted while it has an animation to finish, so a keyframe that appeared in the same commit as the uncheck would re-pop the check before it vanished.

The indicator reuses godui-fade-scale-in and overrides the start scale locally with --godui-enter-scale: 0.5, so the check grows from half size instead of the 96% a dialog uses. The keyframe still multiplies by the root-only --godui-motion, so reduced motion turns it back into a fade.

03/03Result
03

The result

Open the menu and toggle the checkboxes and radio items. These items call event.preventDefault() in onSelect, so the menu stays open and you can watch each indicator pop.

What's animated

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
Check / radio appearsgodui-fade-scale-in from 50%opacity, scaleease-spring-snappy260ms
Item highlightsnapsnonenonenone

Why GPU-only

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

Reduced motion

--godui-motion: 0 removes the scale and drift from both keyframes. The menu and its indicators fade, in 150ms.

Replacing shadcn

npx shadcn add @godui/dropdown-menu overwrites components/ui/dropdown-menu.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.