TLDR: Tailwind v4 changed translate utilities to emit the CSS
translate:property instead oftransform:. If your animation keyframe usestransform, the two compound during the animation and your modal will jump off-center every single open.
The Setup
I was building a center-cut modal for an ecommerce business's org chart — a classic position: fixed; top: 1/2; left: 1/2; -translate-x-1/2 -translate-y-1/2 with a little scale-in open animation.
Standard stuff. Shipped it. Reviewed it.
Looked great.
The Wall
Except… every time the modal opened, it would flash off to the upper-left corner for a split second, then snap into center.
Every. Single. Time.
My first instinct? I figured my centering math was off. Checked the classes. All looked right. Took a screenshot. Screenshot looked perfectly centered.
That's the trap right there. A screenshot taken after the animation settles shows a centered modal. The bug only lives during the animation — 160 milliseconds you'd miss unless you're watching for it.
What Was Actually Happening
Tailwind v4 quietly changed the behavior of -translate-x-1/2 and -translate-y-1/2.
In v3, those classes emitted transform: translate(var(--tw-translate-x), var(--tw-translate-y)) — everything on the transform property.
In v4, they emit the standalone CSS translate: property. As in, literally translate: -50% -50%.
That is a completely separate CSS property from transform.
So what? GREAT question.
My open animation was a @keyframes that animated transform: translate(-50%, -50%) scale(0.96). That's the transform property.
Now I had two separate properties stacking at the same time:
translate: -50% -50%(from Tailwind utilities)transform: translate(-50%, -50%) scale(...)(from the keyframe)
They compound. During the animation you get roughly -100%, -100% offset. The element shoots to the upper left. When the animation ends, the keyframe's transform releases, only the translate: property survives, and the element snaps back to where it should have been the whole time.
The Fix That Actually Worked
Drop the Tailwind translate utilities entirely. Set the centering via an inline style on the same property the keyframe uses:
<Dialog.Content
style={{ transform: "translate(-50%, -50%)" }}
className="fixed top-1/2 left-1/2 ... data-[state=open]:animate-[popIn_160ms_...]"
/>
top-1/2 and left-1/2 are totally fine — they use the top and left CSS properties, not transform. No compounding.
Now the base position and the keyframe both speak transform. They don't stack. The modal opens clean.
Why I Verify in the Browser Now (Not in Source)
The real lesson here isn't just the utility change — it's that a static screenshot told me everything was fine.
I had to open Arc (my browser, with remote debugging enabled) and actually run getComputedStyle(el).translate and check getBoundingClientRect() against the viewport center to catch this.
The source code looked right. The end state looked right. Only the rendered CSS in a live frame told the truth.
On any v3→v4 Tailwind upgrade: translate, scale, and rotate utilities now emit individual CSS properties. If you're animating transform in a keyframe and centering with translate utilities, you're now running two separate systems that compound. Pick one. I always pick transform now.
P.S. The org chart shipped. Modal opens butter-smooth. Zero flicker.