GodUIGodUI
129Follow on X

Anatomy of the GodUI Dialog

Two keyframes on Radix state, a spring written as CSS linear(), and why the scale never fights the centering translate.

01/04The entrance
opacity
0 → 1
scale
0.96 → 1
tsx
<DialogPrimitive.Contentdata-slot="dialog-content"className={cn(  "fixed top-[50%] left-[50%] z-50 … translate-x-[-50%] translate-y-[-50%] …   data-[state=open]:animate-godui-fade-scale-in   data-[state=closed]:animate-godui-fade-scale-out …",  className,)}/>
01

The entrance

Radix sets data-state="open" on the content when the dialog opens and "closed" when it starts closing. GodUI hangs one keyframe on each state.

godui-fade-scale-in only defines a from frame: opacity: 0 and scale: 0.96. The browser animates from there to the element's own values, so the dialog lands exactly where its classes put it.

The dialog is centered with translate-x-[-50%] translate-y-[-50%], which Tailwind v4 compiles to the individual translate property. The keyframe animates scale, a different property, so the two compose instead of the animation overwriting the centering offset.

02/04The spring

ease-spring-snappy

tsx
--animate-godui-fade-scale-in: godui-fade-scale-invar(--godui-duration-base) var(--ease-spring-snappy) both;@keyframes godui-fade-scale-in {from {  opacity: 0;  scale: calc(1 - (1 - var(--godui-enter-scale)) * var(--godui-motion));}}
02

The spring

The entrance runs for 260ms on ease-spring-snappy, a critically damped spring sampled into a CSS linear() curve. It accelerates hard and eases into place with a barely visible settle.

The exit is shorter: 150ms on ease-out-expo, because nobody waits for a dialog to finish closing.

03/04Waiting for the exit
opacity
1 → 0
scale
1 → 0.96
tsx
// Radix Presence, simplifiedif (!present && getAnimationName(node) !== "none") {// keep the node mounted until 'animationend'}
03

Waiting for the exit

A closing dialog has to stay in the DOM long enough to animate out. Radix's Presence handles that: when open becomes false it checks whether the content has a CSS animation running, and if so it waits for animationend before unmounting.

That's why the exit is a keyframe on data-[state=closed] rather than a transition: Presence watches animations, not transitions.

04/04Result
04

The result

Open the dialog, then close it with Escape, the Cancel button or a click on the overlay. All three paths run the same exit keyframe.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
Opengodui-fade-scale-inopacity, scale 0.96 → 1ease-spring-snappy260ms
Closegodui-fade-scale-outopacity, scale 1 → 0.96ease-out-expo150ms
Overlaygodui-fade-in / godui-fade-outopacitysnappy / expo260 / 150ms

The content's centering translate is untouched: the keyframes animate the separate scale property, so they can't fight the -50% offset.

Why GPU-only

The dialog animates scale and opacity; the overlay animates opacity. shadcn's version does the same through tw-animate-css, so GodUI changes only the curve: a spring in, a fast exit out.

Reduced motion

Under prefers-reduced-motion the root sets --godui-motion: 0. The keyframe multiplies its scale offset by it, so the dialog fades without scaling, and the duration drops to 150ms.

Replacing shadcn

npx shadcn add @godui/dialog overwrites components/ui/dialog.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) and the GodUI button it uses for the footer close button.