GodUIGodUI
129Follow on X

Anatomy of the GodUI Sonner

Sonner moves toasts with transforms, except when it animates height. How GodUI narrows a library's transitions without forking it, and why that needs !important.

01/03What sonner animates
transform
translateY(100%) → none
opacity
0 → 1
height
front toast → own (snaps)
tsx
/* sonner/dist/styles.css */[data-sonner-toast] {transition: transform 400ms, opacity 400ms, height 400ms, box-shadow 200ms;}[data-sonner-toast][data-expanded='false'][data-front='false'] {height: var(--front-toast-height);}
01

What sonner animates

Sonner is mostly compositor-friendly: toasts enter with translateY, stack with scale, and leave with opacity. Two properties aren't. Collapsed toasts behind the front one take its height, and when you hover the stack they grow back to their own height, transitioned over 400ms. box-shadow is on the list too.

A height transition means layout on every frame for every toast in the stack.

02/03Overriding without forking

ease-spring-snappy

tsx
const GODUI_TOAST ="[transition-property:transform,opacity]! ease-spring-snappy!";const GODUI_BUTTON = "[transition-property:opacity]!";<SonnertoastOptions={{  ...toastOptions,  classNames: {    ...toastOptions?.classNames,    toast: cn(GODUI_TOAST, toastOptions?.classNames?.toast),    actionButton: cn(GODUI_BUTTON, toastOptions?.classNames?.actionButton),    // cancelButton, closeButton: same  },}}{...props}/>
02

Overriding without forking

GodUI doesn't fork sonner. It passes classes through toastOptions.classNames that change only transition-property (to transform, opacity) and the easing (to ease-spring-snappy). Durations stay sonner's. Heights now change in one step while the transforms still glide.

The ! matters. Sonner injects its CSS without a cascade layer, and Tailwind puts utilities in @layer utilities. Unlayered rules beat layered ones no matter how specific the layered selector is, so only !important wins.

Forcing only the property list also keeps swiping right. During a drag, sonner sets transition: none, which zeroes the duration, so the toast still tracks your finger 1:1.

03/03Result
03

The result

Show three or four toasts, then hover the stack to expand it. The toasts spread out on a spring and their heights change in one step.

What's animated

InteractionKeyframe / mechanismPropertiesEasingDuration
Enter, exit, stacksonner's transitionstransform, opacityease-spring-snappy400ms
Stack expand (heights)snapsnonenonenone
Swipe to dismisspointer-driven transformtransformnonelive
Action, cancel, close buttonssonner's transitionopacitynonesonner's

Sonner injects its stylesheet unlayered, so GodUI's overrides use !important; only transition-property and the easing are forced.

Why GPU-only

Every toast transition is transform or opacity, and the buttons only fade. A Playwright test expands a stack of mixed-height toasts and fails if Chrome reports a height animation, and it does fail without GodUI's overrides.

Reduced motion

Sonner's own prefers-reduced-motion styles still apply: they turn off its transitions and animations, and GodUI doesn't override that.

Replacing shadcn

npx shadcn add @godui/sonner overwrites components/ui/sonner.tsx. Exports and props match shadcn/ui new-york-v4 (Toaster, with the same icons, theme handling and CSS variables), so existing imports keep working. Keep calling toast() from sonner.

If you pass toastOptions.classNames, GodUI merges its own classes with yours rather than replacing them.