Web development
Animations with Tailwind, no JavaScript
Tailwind animates with CSS transitions and keyframes in the theme. No JavaScript runs, but exit animations need extra care.
Key points
- transition-*, duration-* and animate-* compile to CSS transitions and keyframes, with no browser runtime.
- In Tailwind 4 a new animation is declared in the theme with --animate-* and @keyframes in the same block.
- None of the 97 cached shadcn/ui blocks uses animate-*: animation stays a manual decision.
Tailwind CSS animates with CSS alone: transition-*, duration-* and animate-* compile to transitions and keyframes, so the browser runs no JavaScript to move an element.
How to declare an animation
In Tailwind 4 the theme lives in CSS: a --animate-name variable creates the animate-name utility, and the @keyframes sit in the same @theme block. No tailwind.config.js is needed.
@import "tailwindcss";
@theme {
--animate-slide-in: slide-in 240ms ease-out;
@keyframes slide-in {
from { opacity: 0; transform: translateY(8px); }
to { opacity: 1; transform: translateY(0); }
}
}
After the build the element uses class="animate-slide-in" and the browser applies the keyframes without listening for events. The tailwindcss.com documentation, read on 1 November 2026, describes --animate-* as a theme namespace.
npx @tailwindcss/cli -i ./src/input.css -o ./dist/output.css --watch
The command needs Node and rewrites dist/output.css on every save; src/input.css has to start with @import "tailwindcss";.
State transitions, not timelines
Hover, focus and theme changes need only transition-colors and duration-200: the property changes and the browser interpolates. For an entrance tied to an attribute such as data-state="open", the variant data-[state=open]:animate-slide-in removes the orchestration JavaScript.
- A CSS transition never blocks the main thread and adds no kilobytes of JavaScript.
- With
motion-reduce:animate-noneyou honourprefers-reduced-motion, and the manual test stays mandatory. - The 97 cached blocks animate nothing by default:
animate-*appears 0 times. - Source: the tailwindcss.com documentation and the MIT licence in the repository, read on 1 November 2026.
Where it breaks
The exit is the hard part: if the node leaves the DOM the transition never starts. You need transition-behavior: allow-discrete and @starting-style, or a transitionend listener that unmounts the node after the last frame. Tailwind sequences nothing and delays no unmount: that decision belongs to the component.
Where to continue
The EneUI catalogue holds typewriter, typing-animation and marquee along an SVG path, all built with Tailwind classes. The definition of the framework is in what Tailwind CSS is; the library comparison is in the best animation libraries; the comparison of CSS approaches is in Tailwind against CSS modules. The other two pieces in the series are focus-visible with Tailwind and print styles with Tailwind.
Frequently asked questions
Do I need JavaScript to animate with Tailwind?
No. The classes compile to CSS transitions and keyframes, which the browser runs on its own. The JavaScript you write stays the component's, not the animation's.
What is the difference between transition-* and animate-*?
transition-* interpolates a property change that already happened, on hover for example. animate-* plays keyframes once or in a loop, whatever the state change.
How do I add a custom animation?
Declare --animate-name inside the @theme block and put the @keyframes in the same block. The build generates the animate-name utility, which you use like any other class.
Does Tailwind honour prefers-reduced-motion?
Yes, through the motion-reduce variant, for example motion-reduce:animate-none. The theme never applies it on its own: you write it on each animated element.
Why is my animation not running?
Usually the node leaves the DOM before the transition ends, or the class is built at runtime and never reaches the CSS file. Check the class in the generated file and the moment the node unmounts.