Transitions, Transforms & Animations
Small movements make an interface feel alive and help people follow what changed: a button that eases into its hover colour, a card that lifts slightly, a spinner that shows something is loading, a menu that fades in instead of blinking into place. Tailwind covers all of these with utilities, and you can add your own animations in CSS.
In this lesson you will add transitions, move and scale elements with transforms, use the built-in animations, define a custom animation with @theme, animate elements as they appear with the starting: variant, and respect visitors who prefer less motion.
Transitions
A transition makes a property change smoothly over time instead of jumping. Put the transition classes on the element in its normal state, and the change itself behind a variant such as hover::
<button class="bg-sky-600 transition-colors duration-200 ease-out hover:bg-sky-700">Apply now</button>| Class | What it animates |
|---|---|
transition | Colours, opacity, shadows, transforms, filters and a few more |
transition-colors | Only colour properties |
transition-opacity | Only opacity |
transition-transform | Only scale, rotate, translate and transform |
transition-shadow | Only box shadow |
transition-all | Every property that can animate |
transition-none | Nothing |
The default duration is 150ms with a gentle ease-in-out curve. Change them with duration-300 (any number of milliseconds), ease-linear, ease-in, ease-out, ease-in-out, and wait before starting with delay-150. Most interface transitions feel best between 150ms and 300ms; longer ones start to feel slow.
Transforms
Transforms move, resize or rotate an element without affecting the layout around it, which makes them smooth to animate:
scale-105grows to 105%;scale-95shrinks to 95%;scale-x-110stretches sideways only.rotate-6turns 6 degrees clockwise;-rotate-3turns 3 degrees the other way.translate-x-4moves 1rem right;-translate-y-1moves 0.25rem up;translate-x-fullmoves by the element's own width.skew-x-6slants the element;origin-topororigin-bottom-leftchanges the point it scales or rotates around.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Hover effects</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="flex flex-wrap items-start gap-6 bg-slate-100 p-8">
<article class="w-56 rounded-xl bg-white p-5 shadow-sm transition duration-300 hover:-translate-y-1 hover:shadow-xl">
<h3 class="font-semibold">Lift on hover</h3>
<p class="text-sm text-slate-600">translate and shadow</p>
</article>
<div class="w-56 overflow-hidden rounded-xl bg-white">
<img src="https://course.ethicscomputer.in/assets/samples/landscape.svg" alt="Backwaters at Alleppey" class="aspect-video w-full object-cover transition-transform duration-500 hover:scale-110">
<p class="p-4 text-sm">Zoom inside a frame</p>
</div>
<button class="rounded-lg bg-amber-500 px-5 py-3 font-semibold text-white transition hover:rotate-2 active:scale-95">Press me</button>
</body>
</html>In Tailwind 4, scale-*, rotate-* and translate-* use the separate CSS properties scale, rotate and translate instead of one combined transform. You can change them independently, and they still combine on one element. Tailwind 4 also added 3D transforms: perspective-normal on a parent, then rotate-x-12, rotate-y-12 or translate-z-* on the child, with transform-3d to keep children in 3D space and backface-hidden for flip cards.
Built-in animations
Tailwind includes four ready-made animations for common needs:
animate-spinrotates forever, for loading spinners.animate-pingscales up and fades out, like a radar blip, for "live" or notification dots.animate-pulsegently fades in and out, for skeleton placeholders while content loads.animate-bouncebounces up and down, for "scroll down" hints.
You can also define your own. An animation is a theme variable in the --animate-* namespace plus the @keyframes it uses, both inside @theme:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Animations</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
<style type="text/tailwindcss">
@theme {
--animate-wiggle: wiggle 0.6s ease-in-out 3;
@keyframes wiggle {
0%, 100% { rotate: -4deg; }
50% { rotate: 4deg; }
}
}
</style>
</head>
<body class="flex flex-wrap items-center gap-10 p-10">
<svg class="size-8 animate-spin text-sky-600" viewBox="0 0 24 24" fill="none" role="img" aria-label="Loading">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" class="opacity-25"/>
<path d="M22 12a10 10 0 0 0-10-10" stroke="currentColor" stroke-width="4" stroke-linecap="round"/>
</svg>
<span class="relative flex size-4">
<span class="absolute inline-flex size-full animate-ping rounded-full bg-red-400 opacity-75"></span>
<span class="relative inline-flex size-4 rounded-full bg-red-600"></span>
</span>
<div class="w-48 animate-pulse space-y-2">
<div class="h-4 rounded bg-slate-200"></div>
<div class="h-4 w-2/3 rounded bg-slate-200"></div>
</div>
<button class="animate-wiggle rounded-lg bg-rose-600 px-4 py-2 text-white">New offer!</button>
</body>
</html>The custom animate-wiggle utility exists because the theme defines --animate-wiggle. It runs three times and stops, since endless movement next to text is distracting.
Animating elements as they appear
Elements that appear, such as a popover, a dialog or newly added list items, have no "before" state to transition from, so normally they just pop in. The starting: variant uses CSS @starting-style to define that before state. Pair it with transition-discrete, which lets display itself take part in the transition, so the element can also fade out before it is hidden.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Popover entry</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="p-10">
<button popovertarget="offer" class="rounded-lg bg-sky-600 px-4 py-2 text-white">Show offer</button>
<div id="offer" popover class="m-auto w-72 rounded-xl bg-white p-6 shadow-2xl opacity-0 scale-95 transition-all transition-discrete duration-300 open:opacity-100 open:scale-100 starting:open:opacity-0 starting:open:scale-95">
<p class="font-semibold">Diwali offer</p>
<p class="mt-1 text-slate-600">20% off all courses until 5 November.</p>
</div>
</body>
</html>Click the button: the popover fades and grows into place, and when you click outside it, it fades back out. open: matches the popover while it is showing, starting:open: gives the state it starts from, and the plain opacity-0 scale-95 is where it goes when closing. The popover attribute itself is explained in Popover API.
Respecting reduced motion
Some people get dizzy or distracted from movement on screen and turn on "reduce motion" in their device settings. Tailwind has two variants for this:
motion-safe:applies only when the visitor has not asked for reduced motion:motion-safe:animate-bounce.motion-reduce:applies only when they have:motion-reduce:transition-none.
Use motion-safe: for decorative movement, such as bouncing and wiggling. Simple colour transitions are usually fine for everyone.
Common mistakes
- Putting
transitionbehindhover:.hover:transitiononly turns the transition on while hovering, so the change back is instant. Puttransitionon the base element. transitionwithout a change. A transition only does something when a property changes, for example through a state variant.- Animating layout properties. Prefer
translate-*andscale-*to changingwidth,topormargin. - Endless decorative animations. Use them rarely, run them a few times, and wrap them in
motion-safe:.
Key points
- Put
transition,duration-*andease-*on the base element; change values with variants such ashover:. scale-*,rotate-*,translate-*andskew-*transform elements without moving their neighbours; v4 adds 3D transforms.animate-spin,animate-ping,animate-pulseandanimate-bouncecover common loading and attention effects.- Define custom animations with an
--animate-*variable and@keyframesinside@theme. starting:withtransition-discreteanimates elements as they appear and disappear.- Respect reduced motion with
motion-safe:andmotion-reduce:.
Exercise
Create a "Download admit card" button for a university in Jaipur. On hover it should lift by 2px and its shadow should grow, with a 200ms transition. Inside the button, show a small spinner icon with animate-spin that only spins when the visitor has not asked for reduced motion. When the button is pressed, it should shrink to 95%.
Show solution
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Download button</title>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4.3.3"></script>
</head>
<body class="p-10">
<button class="flex items-center gap-2 rounded-lg bg-indigo-600 px-5 py-3 font-semibold text-white shadow-md transition duration-200 hover:-translate-y-0.5 hover:shadow-xl active:scale-95">
<svg class="size-5 motion-safe:animate-spin" viewBox="0 0 24 24" fill="none" aria-hidden="true">
<circle cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4" class="opacity-25"/>
<path d="M22 12a10 10 0 0 0-10-10" stroke="currentColor" stroke-width="4"/>
</svg>
Download admit card
</button>
</body>
</html>transition duration-200 sits on the base button so both hovering and leaving are smooth, -translate-y-0.5 lifts it by 2px, and motion-safe:animate-spin keeps the spinner still for visitors who prefer reduced motion.
Quick Quiz
1. Where should the transition class go?
Placed on the base element, the transition applies both when the state starts and when it ends.
2. Which class rotates an element 3 degrees anticlockwise?
A leading minus makes the value negative; negative rotation turns anticlockwise.
3. How do you create a custom animate-float utility in Tailwind 4?
Theme variables in the --animate-* namespace create animate-* utilities; the keyframes go inside @theme too.
4. What does the starting: variant do?
starting: creates @starting-style rules, giving newly shown elements a "before" state to transition from.
Learn Tailwind CSS with a mentor
Free lessons get you started. Our Full Stack Web Development training adds real projects and code reviews from working developers.
- Project-based learning
- Mentor code reviews
- Free counselling call
