Transitions, Transforms & Animations

Tailwind CSS · Lesson 18 of 27 6 min read Updated

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::

HTML
<button class="bg-sky-600 transition-colors duration-200 ease-out hover:bg-sky-700">Apply now</button>
ClassWhat it animates
transitionColours, opacity, shadows, transforms, filters and a few more
transition-colorsOnly colour properties
transition-opacityOnly opacity
transition-transformOnly scale, rotate, translate and transform
transition-shadowOnly box shadow
transition-allEvery property that can animate
transition-noneNothing

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-105 grows to 105%; scale-95 shrinks to 95%; scale-x-110 stretches sideways only.
  • rotate-6 turns 6 degrees clockwise; -rotate-3 turns 3 degrees the other way.
  • translate-x-4 moves 1rem right; -translate-y-1 moves 0.25rem up; translate-x-full moves by the element's own width.
  • skew-x-6 slants the element; origin-top or origin-bottom-left changes the point it scales or rotates around.
HTMLTry it yourself
<!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-spin rotates forever, for loading spinners.
  • animate-ping scales up and fades out, like a radar blip, for "live" or notification dots.
  • animate-pulse gently fades in and out, for skeleton placeholders while content loads.
  • animate-bounce bounces 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:

HTMLTry it yourself
<!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.

HTMLTry it yourself
<!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 transition behind hover:. hover:transition only turns the transition on while hovering, so the change back is instant. Put transition on the base element.
  • transition without a change. A transition only does something when a property changes, for example through a state variant.
  • Animating layout properties. Prefer translate-* and scale-* to changing width, top or margin.
  • Endless decorative animations. Use them rarely, run them a few times, and wrap them in motion-safe:.

Key points

  • Put transition, duration-* and ease-* on the base element; change values with variants such as hover:.
  • scale-*, rotate-*, translate-* and skew-* transform elements without moving their neighbours; v4 adds 3D transforms.
  • animate-spin, animate-ping, animate-pulse and animate-bounce cover common loading and attention effects.
  • Define custom animations with an --animate-* variable and @keyframes inside @theme.
  • starting: with transition-discrete animates elements as they appear and disappear.
  • Respect reduced motion with motion-safe: and motion-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
HTML
<!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?

2. Which class rotates an element 3 degrees anticlockwise?

3. How do you create a custom animate-float utility in Tailwind 4?

4. What does the starting: variant do?

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