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.
↓Scroll to step through it
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.
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.
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.
- transform
- translateY(100%) → none
- opacity
- 0 → 1
- height
- front toast → own (snaps)
/* 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);}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.
- transform
- translateY(100%) → none
- opacity
- 0 → 1
- height
- front toast → own (snaps)
/* 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);}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.
ease-spring-snappy
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}/>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.
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
| Interaction | Keyframe / mechanism | Properties | Easing | Duration |
|---|---|---|---|---|
| Enter, exit, stack | sonner's transitions | transform, opacity | ease-spring-snappy | 400ms |
| Stack expand (heights) | snaps | none | none | none |
| Swipe to dismiss | pointer-driven transform | transform | none | live |
| Action, cancel, close buttons | sonner's transition | opacity | none | sonner'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.