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.
↓Scroll to step through it
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.
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.
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.
- opacity
- 0 → 1
- scale
- 0.96 → 1
- translate
- 0 -4px → 0 0
<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,)}/>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.
- opacity
- 0 → 1
- scale
- 0.96 → 1
- translate
- 0 -4px → 0 0
<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,)}/>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.
- scale
- 0.5 → 1
- opacity
- 0 → 1
<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>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.
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
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Open (menu, sub-menu) | godui-popover-in | opacity, scale 0.96 → 1 from the trigger, translate 4px → 0 | ease-spring-snappy | 260ms |
| Close | godui-popover-out | the same, reversed | ease-out-expo | 150ms |
| Check / radio appears | godui-fade-scale-in from 50% | opacity, scale | ease-spring-snappy | 260ms |
| Item highlight | snaps | none | none | none |
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.